Repository: hikari-py/hikari
Pull request #737 (2021-08-21 10:05:56)

Document new REST Codes
  - Improve HTTPResponseError to make use of the code if available

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1ef35240..6e42da5b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -155,18 +155,28 @@ jobs:
           retention-days: 2
           if-no-files-found: error
 
-  # This job is only used to mark the CI as being done
-  #
   # Allows us to add this as a required check in Github branch rules, as all the
   # other jobs are subject to change
   ci-done:
     needs: [upload-coverage, linting, twemoji, pages]
+    if: always()
 
     runs-on: ubuntu-20.04
 
     steps:
-      - name: Completed
-        run: echo "CI Completed"
+      - name: Mark status based on past job status
+        env:
+          # All new need jobs need to be added here with the prefix "RESULT_"
+          RESULT_UPLOAD_COVERAGE: ${{ needs.upload-coverage.result }}
+          RESULT_LINTING: ${{ needs.linting.result }}
+          RESULT_TWEMOJI: ${{ needs.twemoji.result }}
+          RESULT_PAGES: ${{ needs.pages.result }}
+        run: |
+          if [ "$(env | grep 'RESULT_')" = "$(env | grep "RESULT_" | grep '=success')" ]; then
+            exit 0
+          else
+            exit 1
+          fi
 
   deploy:
     if: github.event_name == 'release'
diff --git a/hikari/errors.py b/hikari/errors.py
index feee3ddf..088734d1 100644
--- a/hikari/errors.py
+++ b/hikari/errors.py
@@ -270,7 +270,25 @@ class RESTErrorCode(int, enums.Enum):
     """Unknown ban provided."""
 
     UNKNOWN_GUILD_TEMPLATE = 10_057
-    """Unknown guild template."""
+    """Unknown guild template provided."""
+
+    UNKNOWN_STICKER = 10_060
+    """Unknown sticker provided."""
+
+    UNKNOWN_INTERACTION = 10_062
+    """Unknown interaction provided."""
+
+    UNKNOWN_APPLICATION_COMMAND = 10_063
+    """Unknown application command provided."""
+
+    UNKNOWN_APPLICATION_COMMAND_PERMISSIONS = 10_066
+    """Unknown application command permissions provided."""
+
+    UNKNOWN_STAGE_INSTANCE = 10_067
+    """Unknown stage instance provided."""
+
+    UNKNOWN_GUILD_WELCOME_SCREEN = 10_069
+    """Unknown guild welcome screen provided."""
 
     EXPLICIT_CONTENT_BLOCKED = 20_009
     """Explicit content cannot be sent to the desired recipient(s)."""
@@ -281,8 +299,14 @@ class RESTErrorCode(int, enums.Enum):
     WRITE_LIMIT_HIT = 20_028
     """The global write limit on a channel has been hit."""
 
-    DISALLOWED_WORDS_IN_PUBLIC_STAGES = 20_031
-    """The stage channel contains disallowed words for public stages."""
+    DISALLOWED_WORDS_FOR_PUBLIC_STAGES = 20_031
+    """The guild contains disallowed words for public stages.
+
+    This may include guild name, guild description or channel names.
+    """
+
+    GUILD_PREMIUM_LEVEL_TOO_LOW = 20_035
+    """The guilds premium level is too low."""
 
     MAXIMUM_GUILDS = 30_001
     """Maximum number of guilds reached (100)."""
@@ -314,15 +338,24 @@ class RESTErrorCode(int, enums.Enum):
     MAXIMUM_ANIMATED_EMOJIS = 30_018
     """Maximum number of animated emojis reached."""
 
-    MAXIMUM_NUMBER_OF_SERVER_MEMBERS_REACHED = 30_019
-    """Maximum number of server members reached."""
+    MAXIMUM_NUMBER_OF_GUILD_MEMBERS_REACHED = 30_019
+    """Maximum number of guild members reached."""
 
     GUILD_ALREADY_HAS_TEMPLATE = 30_031
     """Guild already has a template."""
 
+    MAXIMUM_NUMBER_OF_THREAD_PARTICIPANTS_REACHED = 30_033
+    """Maximum number of thread participants reached."""
+
     MAXIMUM_BANS_FOR_NON_GUILD_MEMBERS = 30_035
     """Maximum number of bans for non-guild members reached."""
 
+    MAXIMUM_NUMBER_OF_STICKERS_REACHED = 30_037
+    """Maximum number of stickers reached."""
+
+    MAXIMUM_PRUNE_REQUESTS_REACHED = 30_040
+    """Maximum number of prune requests has been reached. Try again later."""
+
     REQUEST_TOO_LARGE = 40_005
     """Request too large. Try sending something smaller in size."""
 
@@ -401,6 +434,9 @@ class RESTErrorCode(int, enums.Enum):
     SYSTEM_OVERLOADED = 130_000
     """API resource is currently overloaded. Try again a little later."""
 
+    STAGE_ALREADY_OPEN = 150_006
+    """The stage channel is already open."""
+
 
 @attr.define(auto_exc=True, repr=False, weakref_slot=False)
 class HTTPResponseError(HTTPError):
@@ -409,7 +445,7 @@ class HTTPResponseError(HTTPError):
     url: str = attr.field()
     """The URL that produced this error message."""
 
-    status: typing.Union[int, http.HTTPStatus] = attr.field()
+    status: http.HTTPStatus = attr.field()
     """The HTTP status code for the response."""
 
     headers: data_binding.Headers = attr.field()
@@ -425,11 +461,13 @@ class HTTPResponseError(HTTPError):
     """The error code."""
 
     def __str__(self) -> str:
-        if isinstance(self.status, http.HTTPStatus):
-            name = self.status.name.replace("_", " ").title()
-            name_value = f"{name} {self.status.value}"
+        name = self.status.name.replace("_", " ").title()
+        name_value = f"{name} {self.status.value}"
+
+        if isinstance(self.code, RESTErrorCode) and self.code != RESTErrorCode.GENERAL_ERROR:
+            code_str = f" ({self.code.name})"
         else:
-            name_value = str(self.status).title()
+            code_str = ""
 
         if self.message:
             body = self.message
@@ -441,7 +479,7 @@ def __str__(self) -> str:
 
         chomped = len(body) > 200
 
-        return f"{name_value}: '{body[:200]}{'...' if chomped else ''}' for {self.url}"
+        return f"{name_value}:{code_str} '{body[:200]}{'...' if chomped else ''}' for {self.url}"
 
 
 @attr.define(auto_exc=True, repr=False, weakref_slot=False)
diff --git a/hikari/guilds.py b/hikari/guilds.py
index 4e5ef439..ba8ac61f 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -2991,8 +2991,11 @@ class RESTGuild(Guild):
     This will be `builtins.None` when creating a guild.
     """
 
-    max_presences: int = attr.field(eq=False, hash=False, repr=False)
-    """The maximum number of presences for the guild."""
+    max_presences: typing.Optional[int] = attr.field(eq=False, hash=False, repr=False)
+    """The maximum number of presences for the guild.
+
+    If `builtins.None`, then there is no limit.
+    """
 
     max_members: int = attr.field(eq=False, hash=False, repr=False)
     """The maximum number of members allowed in this guild."""
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index 1487bb3c..80e6ae74 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -61,9 +61,8 @@
 from hikari.internal import data_binding
 from hikari.internal import time
 
-_DEFAULT_MAX_PRESENCES: typing.Final[int] = 25000
-_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.entity_factory")
 _ValueT = typing.TypeVar("_ValueT")
+_LOGGER: typing.Final[logging.Logger] = logging.getLogger("hikari.entity_factory")
 
 
 def _with_int_cast(cast: typing.Callable[[int], _ValueT]) -> typing.Callable[[typing.Any], _ValueT]:
@@ -1372,10 +1371,7 @@ def deserialize_rest_guild(self, payload: data_binding.JSONObject) -> guild_mode
         max_members = int(payload["max_members"])
 
         raw_max_presences = payload["max_presences"]
-        if raw_max_presences is None:
-            max_presences = _DEFAULT_MAX_PRESENCES
-        else:
-            max_presences = int(raw_max_presences)
+        max_presences = int(raw_max_presences) if raw_max_presences is not None else None
 
         roles = {
             snowflakes.Snowflake(role["id"]): self.deserialize_role(role, guild_id=guild_fields.id)
diff --git a/hikari/internal/aio.py b/hikari/internal/aio.py
index 20bbdd63..b7731331 100644
--- a/hikari/internal/aio.py
+++ b/hikari/internal/aio.py
@@ -205,7 +205,7 @@ def get_or_make_loop() -> asyncio.AbstractEventLoop:
     # get_event_loop will error under oddly specific cases such as if set_event_loop has been called before even
     # if it was just called with None or if it's called on a thread which isn't the main Thread.
     try:
-        loop = asyncio.get_event_loop()
+        loop = asyncio.get_event_loop_policy().get_event_loop()
 
         # Closed loops cannot be re-used.
         if not loop.is_closed():
diff --git a/hikari/internal/time.py b/hikari/internal/time.py
index 20e85c60..316bdcba 100644
--- a/hikari/internal/time.py
+++ b/hikari/internal/time.py
@@ -87,13 +87,12 @@ def slow_iso8601_datetime_string_to_datetime(datetime_str: str) -> datetime.date
     return datetime.datetime.fromisoformat(datetime_str)
 
 
+fast_iso8601_datetime_string_to_datetime: typing.Optional[typing.Callable[[str], datetime.datetime]]
 try:
     # CISO8601 is around 600x faster than modules like dateutil, which is
     # going to be noticable on big bots where you are parsing hundreds of
     # thousands of "joined_at" fields on users on startup.
-    #
-    # ciso8601 doesn't have typing available, so ignore it.
-    import ciso8601  # type: ignore[import]
+    import ciso8601
 
     # Discord appears to actually use RFC-3339, which isn't a true ISO-8601 implementation,
     # but somewhat of a subset with some edge cases.
diff --git a/hikari/internal/ux.py b/hikari/internal/ux.py
index 1dc6c768..b33b05cb 100644
--- a/hikari/internal/ux.py
+++ b/hikari/internal/ux.py
@@ -36,7 +36,7 @@
 import typing
 import warnings
 
-import colorlog  # type: ignore[import]
+import colorlog
 
 from hikari import _about as about
 from hikari.internal import net
@@ -191,9 +191,9 @@ def print_banner(package: typing.Optional[str], allow_color: bool, force_color:
     }
 
     if supports_color(allow_color, force_color):
-        args.update(colorlog.escape_codes)
+        args.update(colorlog.escape_codes.escape_codes)
     else:
-        for code in colorlog.escape_codes:
+        for code in colorlog.escape_codes.escape_codes:
             args[code] = ""
 
     sys.stdout.write(string.Template(raw_banner).safe_substitute(args))
diff --git a/pipelines/mypy.nox.py b/pipelines/mypy.nox.py
index f2c1cedb..0502b3d8 100644
--- a/pipelines/mypy.nox.py
+++ b/pipelines/mypy.nox.py
@@ -35,7 +35,7 @@
 @nox.session(reuse_venv=True)
 def mypy(session: nox.Session) -> None:
     """Perform static type analysis on Python source code."""
-    session.install("-r", "requirements.txt", "-r", "dev-requirements.txt")
+    session.install("-r", "requirements.txt", "-r", "speedup-requirements.txt", "-r", "dev-requirements.txt")
 
     _generate_stubs(session)
 
@@ -46,7 +46,7 @@ def mypy(session: nox.Session) -> None:
 @nox.session(reuse_venv=True)
 def generate_stubs(session: nox.Session) -> None:
     """Generate the stubs for the package."""
-    session.install("-r", "requirements.txt", "-r", "dev-requirements.txt")
+    session.install("-r", "dev-requirements.txt")
     _generate_stubs(session)
 
 
diff --git a/requirements.txt b/requirements.txt
index f6f67465..ef3960f9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 aiohttp==3.7.4.post0
 attrs==21.2.0
-colorlog==5.0.1
+colorlog==6.4.1
 multidict==5.1.0
 pure25519==0.0.1


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index 262e48b5..b209eb30 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -2411,7 +2411,7 @@ def test_deserialize_rest_guild_with_null_fields(self, entity_factory_impl):
         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 entity_factory._DEFAULT_MAX_PRESENCES
+        assert guild.max_presences is None
         assert guild.vanity_url_code is None
         assert guild.description is None
         assert guild.banner_hash is None
diff --git a/tests/hikari/impl/test_rest_bot.py b/tests/hikari/impl/test_rest_bot.py
index 91f67cdb..26078818 100644
--- a/tests/hikari/impl/test_rest_bot.py
+++ b/tests/hikari/impl/test_rest_bot.py
@@ -297,7 +297,7 @@ def test_run(self, mock_rest_bot):
             ssl_context=mock_context,
         )
         mock_rest_bot.join.assert_awaited_once()
-        assert asyncio.get_event_loop().is_closed() is False
+        assert asyncio.get_event_loop_policy().get_event_loop().is_closed() is False
 
     def test_run_when_asyncio_debug(self, mock_rest_bot):
         mock_rest_bot.start = mock.Mock()
diff --git a/tests/hikari/internal/test_aio.py b/tests/hikari/internal/test_aio.py
index aad61eb1..8cf1eca4 100644
--- a/tests/hikari/internal/test_aio.py
+++ b/tests/hikari/internal/test_aio.py
@@ -398,7 +398,7 @@ def test_get_or_make_loop_handles_runtime_error():
 
         new_event_loop.assert_called_once_with()
 
-    assert asyncio.get_event_loop() is mock_loop
+    assert asyncio.get_event_loop_policy().get_event_loop() is mock_loop
 
 
 def test_get_or_make_loop_handles_closed_loop():
@@ -410,4 +410,4 @@ def test_get_or_make_loop_handles_closed_loop():
 
         new_event_loop.assert_called_once_with()
 
-    assert asyncio.get_event_loop() is mock_loop
+    assert asyncio.get_event_loop_policy().get_event_loop() is mock_loop
diff --git a/tests/hikari/internal/test_ux.py b/tests/hikari/internal/test_ux.py
index 7c76df4b..f8e52ccb 100644
--- a/tests/hikari/internal/test_ux.py
+++ b/tests/hikari/internal/test_ux.py
@@ -153,7 +153,9 @@ def mock_args(self):
 
     def test_when_supports_color(self, mock_args):
         stack = contextlib.ExitStack()
-        stack.enter_context(mock.patch.object(colorlog, "escape_codes", new={"red": 0, "green": 1, "blue": 2}))
+        stack.enter_context(
+            mock.patch.object(colorlog.escape_codes, "escape_codes", new={"red": 0, "green": 1, "blue": 2})
+        )
         supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=True))
         read_text = stack.enter_context(mock.patch.object(importlib.resources, "read_text"))
         template = stack.enter_context(mock.patch.object(string, "Template"))
@@ -191,7 +193,9 @@ def test_when_supports_color(self, mock_args):
 
     def test_when_doesnt_supports_color(self, mock_args):
         stack = contextlib.ExitStack()
-        stack.enter_context(mock.patch.object(colorlog, "escape_codes", new={"red": 0, "green": 1, "blue": 2}))
+        stack.enter_context(
+            mock.patch.object(colorlog.escape_codes, "escape_codes", new={"red": 0, "green": 1, "blue": 2})
+        )
         supports_color = stack.enter_context(mock.patch.object(ux, "supports_color", return_value=False))
         read_text = stack.enter_context(mock.patch.object(importlib.resources, "read_text"))
         template = stack.enter_context(mock.patch.object(string, "Template"))
diff --git a/tests/hikari/test_errors.py b/tests/hikari/test_errors.py
index 0c067efe..875dfcee 100644
--- a/tests/hikari/test_errors.py
+++ b/tests/hikari/test_errors.py
@@ -75,19 +75,29 @@ def test_str(self, error):
 class TestHTTPResponseError:
     @pytest.fixture()
     def error(self):
-        return errors.HTTPResponseError("https://some.url", http.HTTPStatus.BAD_REQUEST, {}, "raw body")
+        return errors.HTTPResponseError(
+            "https://some.url",
+            http.HTTPStatus.BAD_REQUEST,
+            {},
+            "raw body",
+            "message",
+            errors.RESTErrorCode.UNKNOWN_GUILD,
+        )
 
     def test_str(self, error):
-        assert str(error) == "Bad Request 400: 'raw body' for https://some.url"
+        assert str(error) == "Bad Request 400: (UNKNOWN_GUILD) 'message' for https://some.url"
+
+    def test_str_when_message_is_None(self, error):
+        error.message = None
+        assert str(error) == "Bad Request 400: (UNKNOWN_GUILD) 'raw body' for https://some.url"
 
-    def test_str_when_status_is_not_HTTPStatus(self, error):
-        error.status = "SOME STATUS"
-        assert str(error) == "Some Status: 'raw body' for https://some.url"
+    def test_str_when_code_is_GENERAL_ERROR(self, error):
+        error.code = errors.RESTErrorCode.GENERAL_ERROR
+        assert str(error) == "Bad Request 400: 'message' for https://some.url"
 
-    def test_str_when_message_is_not_None(self, error):
-        error.status = "SOME STATUS"
-        error.message = "Some message"
-        assert str(error) == "Some Status: 'Some message' for https://some.url"
+    def test_str_when_code_is_int(self, error):
+        error.code = 100
+        assert str(error) == "Bad Request 400: 'message' for https://some.url"
 
 
 class TestBadRequestError: