diff options
author | 2022-09-18 19:10:24 +0200 | |
---|---|---|
committer | 2022-09-18 19:14:08 +0200 | |
commit | b6f033e7f5fcdb827e7fed29a4ed21108e54a414 (patch) | |
tree | 99be74f8d90217e8d2dbeba442afce7ea04d5de6 | |
parent | ensure tuples from pos arg and kwarg tuples are differentiated (diff) | |
parent | Merge pull request #138 from python-discord/bump-d.py (diff) |
Merge remote-tracking branch 'upstream/main' into no-duplicate-deco
-rw-r--r-- | .dockerignore | 9 | ||||
-rw-r--r-- | .github/workflows/docs.yaml | 17 | ||||
-rw-r--r-- | .github/workflows/lint-test.yaml | 10 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | botcore/_bot.py | 21 | ||||
-rw-r--r-- | botcore/site_api.py | 5 | ||||
-rw-r--r-- | botcore/utils/__init__.py | 16 | ||||
-rw-r--r-- | botcore/utils/_monkey_patches.py | 5 | ||||
-rw-r--r-- | botcore/utils/commands.py | 38 | ||||
-rw-r--r-- | botcore/utils/cooldown.py | 3 | ||||
-rw-r--r-- | botcore/utils/function.py | 3 | ||||
-rw-r--r-- | botcore/utils/interactions.py | 98 | ||||
-rw-r--r-- | botcore/utils/members.py | 9 | ||||
-rw-r--r-- | botcore/utils/regex.py | 7 | ||||
-rw-r--r-- | botcore/utils/scheduling.py | 32 | ||||
-rw-r--r-- | dev/Dockerfile | 23 | ||||
-rw-r--r-- | dev/README.rst | 53 | ||||
-rw-r--r-- | dev/bot/__init__.py | 24 | ||||
-rw-r--r-- | dev/bot/__main__.py | 34 | ||||
-rw-r--r-- | dev/bot/cog.py | 33 | ||||
-rw-r--r-- | dev/docker-compose.yaml | 27 | ||||
-rw-r--r-- | docker-compose.yaml | 80 | ||||
-rw-r--r-- | docs/changelog.rst | 54 | ||||
-rw-r--r-- | docs/development.rst | 2 | ||||
-rw-r--r-- | docs/index.rst | 2 | ||||
-rw-r--r-- | docs/utils.py | 1 | ||||
-rw-r--r-- | poetry.lock | 380 | ||||
-rw-r--r-- | pyproject.toml | 33 | ||||
-rw-r--r-- | tests/botcore/utils/test_regex.py | 69 | ||||
-rw-r--r-- | tox.ini | 2 |
30 files changed, 702 insertions, 391 deletions
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..9fb3df72 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +* + +!botcore/ +!docs/ +!tests/ + +!pyproject.toml +!poetry.lock +!tox.ini diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 5254d524..42c9e742 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -11,7 +11,6 @@ concurrency: group: docs-deployment-${{ github.ref }} cancel-in-progress: true - jobs: latest-build: # We only need to verify that the docs build with no warnings here @@ -22,16 +21,12 @@ jobs: - uses: actions/checkout@v2 - name: Install Python Dependencies - uses: HassanAbouelela/actions/setup-python@setup-python_v1.1.0 + uses: HassanAbouelela/actions/setup-python@setup-python_v1.3.1 with: dev: true - python_version: 3.9 + python_version: "3.10" install_args: "--extras async-rediscache" - # Undeclared dependency for `releases`... whoops - # https://github.com/bitprophet/releases/pull/82 - - run: pip install six - - name: Generate HTML Site run: sphinx-build -nW -j auto -b html docs docs/build @@ -51,16 +46,12 @@ jobs: fetch-depth: 0 # We need to check out the entire repository to find all tags - name: Install Python Dependencies - uses: HassanAbouelela/actions/setup-python@setup-python_v1.1.0 + uses: HassanAbouelela/actions/setup-python@setup-python_v1.3.1 with: dev: true - python_version: 3.9 + python_version: "3.10" install_args: "--extras async-rediscache" - # Undeclared dependency for `releases`... whoops - # https://github.com/bitprophet/releases/pull/82 - - run: pip install six - - name: Build All Doc Versions run: sphinx-multiversion docs docs/build -n -j auto env: diff --git a/.github/workflows/lint-test.yaml b/.github/workflows/lint-test.yaml index a51623cb..e9821677 100644 --- a/.github/workflows/lint-test.yaml +++ b/.github/workflows/lint-test.yaml @@ -14,21 +14,19 @@ jobs: lint: name: Run Linting & Test Suites runs-on: ubuntu-latest - steps: - name: Install Python Dependencies - uses: HassanAbouelela/actions/setup-python@setup-python_v1.1.0 + uses: HassanAbouelela/actions/setup-python@setup-python_v1.3.1 with: # Set dev=true to run pre-commit which is a dev dependency dev: true - python_version: 3.9 + python_version: "3.10" install_args: "--extras async-rediscache" # We will not run `flake8` here, as we will use a separate flake8 - # action. As pre-commit does not support user installs, we set - # PIP_USER=0 to not do a user install. + # action. - name: Run pre-commit hooks - run: export PIP_USER=0; SKIP=flake8 pre-commit run --all-files + run: SKIP=flake8 pre-commit run --all-files # Run flake8 and have it format the linting errors in the format of # the GitHub Workflow command to register error annotations. This @@ -134,3 +134,6 @@ dmypy.json # Vscode .vscode + +# Development & prototyping environment +/bot/ diff --git a/botcore/_bot.py b/botcore/_bot.py index e9eba5c5..bb25c0b5 100644 --- a/botcore/_bot.py +++ b/botcore/_bot.py @@ -51,7 +51,7 @@ class BotBase(commands.Bot): Initialise the base bot instance. Args: - guild_id: The ID of the guild use for :func:`wait_until_guild_available`. + guild_id: The ID of the guild used for :func:`wait_until_guild_available`. allowed_roles: A list of role IDs that the bot is allowed to mention. http_session (aiohttp.ClientSession): The session to use for the bot. redis_session: The `async_rediscache.RedisSession`_ to use for the bot. @@ -197,7 +197,7 @@ class BotBase(commands.Bot): if not guild.roles or not guild.members or not guild.channels: msg = "Guild available event was dispatched but the cache appears to still be empty!" - self.log_to_dev_log(msg) + await self.log_to_dev_log(msg) return self._guild_available.set() @@ -234,14 +234,16 @@ class BotBase(commands.Bot): ) self.http.connector = self._connector - if getattr(self, "redis_session", False) and self.redis_session.closed: + if getattr(self, "redis_session", False) and not self.redis_session.valid: # If the RedisSession was somehow closed, we try to reconnect it # here. Normally, this shouldn't happen. - await self.redis_session.connect() + await self.redis_session.connect(ping=True) - # Create dummy stats client first, in case `statsd_url` is unreachable within `_connect_statsd()` + # Create dummy stats client first, in case `statsd_url` is unreachable or None self.stats = AsyncStatsClient(loop, "127.0.0.1") - self._connect_statsd(self.statsd_url, loop) + if self.statsd_url: + self._connect_statsd(self.statsd_url, loop) + await self.stats.create_socket() try: @@ -249,7 +251,7 @@ class BotBase(commands.Bot): except Exception as e: raise StartupError(e) - async def ping_services() -> None: + async def ping_services(self) -> None: """Ping all required services on setup to ensure they are up before starting.""" ... @@ -279,11 +281,8 @@ class BotBase(commands.Bot): if self._resolver: await self._resolver.close() - if self.stats._transport: + if getattr(self.stats, "_transport", False): self.stats._transport.close() - if getattr(self, "redis_session", False): - await self.redis_session.close() - if self._statsd_timerhandle: self._statsd_timerhandle.cancel() diff --git a/botcore/site_api.py b/botcore/site_api.py index dbdf4f3b..44309f9d 100644 --- a/botcore/site_api.py +++ b/botcore/site_api.py @@ -26,7 +26,7 @@ class ResponseCodeError(ValueError): Args: response (:obj:`aiohttp.ClientResponse`): The response object from the request. response_json: The JSON response returned from the request, if any. - request_text: The text of the request, if any. + response_text: The text of the request, if any. """ self.status = response.status self.response_json = response_json or {} @@ -76,7 +76,8 @@ class APIClient: """Close the aiohttp session.""" await self.session.close() - async def maybe_raise_for_status(self, response: aiohttp.ClientResponse, should_raise: bool) -> None: + @staticmethod + async def maybe_raise_for_status(response: aiohttp.ClientResponse, should_raise: bool) -> None: """ Raise :exc:`ResponseCodeError` for non-OK response if an exception should be raised. diff --git a/botcore/utils/__init__.py b/botcore/utils/__init__.py index cfc5e99d..09aaa45f 100644 --- a/botcore/utils/__init__.py +++ b/botcore/utils/__init__.py @@ -1,6 +1,18 @@ """Useful utilities and tools for Discord bot development.""" -from botcore.utils import _monkey_patches, caching, channel, cooldown, function, logging, members, regex, scheduling +from botcore.utils import ( + _monkey_patches, + caching, + channel, + commands, + cooldown, + function, + interactions, + logging, + members, + regex, + scheduling, +) from botcore.utils._extensions import unqualify @@ -24,8 +36,10 @@ __all__ = [ apply_monkey_patches, caching, channel, + commands, cooldown, function, + interactions, logging, members, regex, diff --git a/botcore/utils/_monkey_patches.py b/botcore/utils/_monkey_patches.py index f2c6c100..c2f8aa10 100644 --- a/botcore/utils/_monkey_patches.py +++ b/botcore/utils/_monkey_patches.py @@ -1,6 +1,7 @@ """Contains all common monkey patches, used to alter discord to fit our needs.""" import logging +import typing from datetime import datetime, timedelta from functools import partial, partialmethod @@ -46,9 +47,9 @@ def _patch_typing() -> None: log.debug("Patching send_typing, which should fix things breaking when Discord disables typing events. Stay safe!") original = http.HTTPClient.send_typing - last_403 = None + last_403: typing.Optional[datetime] = None - async def honeybadger_type(self, channel_id: int) -> None: # noqa: ANN001 + async def honeybadger_type(self: http.HTTPClient, channel_id: int) -> None: nonlocal last_403 if last_403 and (datetime.utcnow() - last_403) < timedelta(minutes=5): log.warning("Not sending typing event, we got a 403 less than 5 minutes ago.") diff --git a/botcore/utils/commands.py b/botcore/utils/commands.py new file mode 100644 index 00000000..7afd8137 --- /dev/null +++ b/botcore/utils/commands.py @@ -0,0 +1,38 @@ +from typing import Optional + +from discord import Message +from discord.ext.commands import BadArgument, Context, clean_content + + +async def clean_text_or_reply(ctx: Context, text: Optional[str] = None) -> str: + """ + Cleans a text argument or replied message's content. + + Args: + ctx: The command's context + text: The provided text argument of the command (if given) + + Raises: + :exc:`discord.ext.commands.BadArgument` + `text` wasn't provided and there's no reply message / reply message content. + + Returns: + The cleaned version of `text`, if given, else replied message. + """ + clean_content_converter = clean_content(fix_channel_mentions=True) + + if text: + return await clean_content_converter.convert(ctx, text) + + if ( + (replied_message := getattr(ctx.message.reference, "resolved", None)) # message has a cached reference + and isinstance(replied_message, Message) # referenced message hasn't been deleted + ): + if not (content := ctx.message.reference.resolved.content): + # The referenced message doesn't have a content (e.g. embed/image), so raise error + raise BadArgument("The referenced message doesn't have a text content.") + + return await clean_content_converter.convert(ctx, content) + + # No text provided, and either no message was referenced or we can't access the content + raise BadArgument("Couldn't find text to clean. Provide a string or reply to a message to use its content.") diff --git a/botcore/utils/cooldown.py b/botcore/utils/cooldown.py index b9149b48..ee65033d 100644 --- a/botcore/utils/cooldown.py +++ b/botcore/utils/cooldown.py @@ -7,10 +7,9 @@ import random import time import typing import weakref -from collections.abc import Awaitable, Hashable, Iterable +from collections.abc import Awaitable, Callable, Hashable, Iterable from contextlib import suppress from dataclasses import dataclass -from typing import Callable # sphinx-autodoc-typehints breaks with collections.abc.Callable import discord from discord.ext.commands import CommandError, Context diff --git a/botcore/utils/function.py b/botcore/utils/function.py index e8d24e90..0e90d4c5 100644 --- a/botcore/utils/function.py +++ b/botcore/utils/function.py @@ -5,8 +5,7 @@ from __future__ import annotations import functools import types import typing -from collections.abc import Sequence, Set -from typing import Callable # sphinx-autodoc-typehints breaks with collections.abc.Callable +from collections.abc import Callable, Sequence, Set __all__ = ["command_wraps", "GlobalNameConflictError", "update_wrapper_globals"] diff --git a/botcore/utils/interactions.py b/botcore/utils/interactions.py new file mode 100644 index 00000000..26bd92f2 --- /dev/null +++ b/botcore/utils/interactions.py @@ -0,0 +1,98 @@ +import contextlib +from typing import Optional, Sequence + +from discord import ButtonStyle, Interaction, Message, NotFound, ui + +from botcore.utils.logging import get_logger + +log = get_logger(__name__) + + +class ViewWithUserAndRoleCheck(ui.View): + """ + A view that allows the original invoker and moderators to interact with it. + + Args: + allowed_users: A sequence of user's ids who are allowed to interact with the view. + allowed_roles: A sequence of role ids that are allowed to interact with the view. + timeout: Timeout in seconds from last interaction with the UI before no longer accepting input. + If ``None`` then there is no timeout. + message: The message to remove the view from on timeout. This can also be set with + ``view.message = await ctx.send( ... )``` , or similar, after the view is instantiated. + """ + + def __init__( + self, + *, + allowed_users: Sequence[int], + allowed_roles: Sequence[int], + timeout: Optional[float] = 180.0, + message: Optional[Message] = None + ) -> None: + super().__init__(timeout=timeout) + self.allowed_users = allowed_users + self.allowed_roles = allowed_roles + self.message = message + + async def interaction_check(self, interaction: Interaction) -> bool: + """ + Ensure the user clicking the button is the view invoker, or a moderator. + + Args: + interaction: The interaction that occurred. + """ + if interaction.user.id in self.allowed_users: + log.trace( + "Allowed interaction by %s (%d) on %d as they are an allowed user.", + interaction.user, + interaction.user.id, + interaction.message.id, + ) + return True + + if any(role.id in self.allowed_roles for role in getattr(interaction.user, "roles", [])): + log.trace( + "Allowed interaction by %s (%d)on %d as they have an allowed role.", + interaction.user, + interaction.user.id, + interaction.message.id, + ) + return True + + await interaction.response.send_message("This is not your button to click!", ephemeral=True) + return False + + async def on_timeout(self) -> None: + """Remove the view from ``self.message`` if set.""" + if self.message: + with contextlib.suppress(NotFound): + # Cover the case where this message has already been deleted by external means + await self.message.edit(view=None) + + +class DeleteMessageButton(ui.Button): + """ + A button that can be added to a view to delete the message containing the view on click. + + This button itself carries out no interaction checks, these should be done by the parent view. + + See :obj:`botcore.utils.interactions.ViewWithUserAndRoleCheck` for a view that implements basic checks. + + Args: + style (:literal-url:`ButtonStyle <https://discordpy.readthedocs.io/en/latest/interactions/api.html#discord.ButtonStyle>`): + The style of the button, set to ``ButtonStyle.secondary`` if not specified. + label: The label of the button, set to "Delete" if not specified. + """ # noqa: E501 + + def __init__( + self, + *, + style: ButtonStyle = ButtonStyle.secondary, + label: str = "Delete", + **kwargs + ): + super().__init__(style=style, label=label, **kwargs) + + async def callback(self, interaction: Interaction) -> None: + """Delete the original message on button click.""" + await interaction.message.delete() diff --git a/botcore/utils/members.py b/botcore/utils/members.py index e89b4618..1536a8d1 100644 --- a/botcore/utils/members.py +++ b/botcore/utils/members.py @@ -1,6 +1,6 @@ """Useful helper functions for interactin with :obj:`discord.Member` objects.""" - import typing +from collections import abc import discord @@ -30,18 +30,19 @@ async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> typing.Op async def handle_role_change( member: discord.Member, - coro: typing.Callable[..., typing.Coroutine], + coro: typing.Callable[[discord.Role], abc.Coroutine], role: discord.Role ) -> None: """ - Await the given ``coro`` with ``member`` as the sole argument. + Await the given ``coro`` with ``role`` as the sole argument. Handle errors that we expect to be raised from :obj:`discord.Member.add_roles` and :obj:`discord.Member.remove_roles`. Args: - member: The member to pass to ``coro``. + member: The member that is being modified for logging purposes. coro: This is intended to be :obj:`discord.Member.add_roles` or :obj:`discord.Member.remove_roles`. + role: The role to be passed to ``coro``. """ try: await coro(role) diff --git a/botcore/utils/regex.py b/botcore/utils/regex.py index 56c50dad..de82a1ed 100644 --- a/botcore/utils/regex.py +++ b/botcore/utils/regex.py @@ -3,6 +3,7 @@ import re DISCORD_INVITE = re.compile( + r"(https?://)?(www\.)?" # Optional http(s) and www. r"(discord([.,]|dot)gg|" # Could be discord.gg/ r"discord([.,]|dot)com(/|slash)invite|" # or discord.com/invite/ r"discordapp([.,]|dot)com(/|slash)invite|" # or discordapp.com/invite/ @@ -10,7 +11,7 @@ DISCORD_INVITE = re.compile( r"discord([.,]|dot)li|" # or discord.li r"discord([.,]|dot)io|" # or discord.io. r"((?<!\w)([.,]|dot))gg" # or .gg/ - r")([/]|slash)" # / or 'slash' + r")(/|slash)" # / or 'slash' r"(?P<invite>\S+)", # the invite code itself flags=re.IGNORECASE ) @@ -32,7 +33,7 @@ FORMATTED_CODE_REGEX = re.compile( r"(?P<code>.*?)" # extract all code inside the markup r"\s*" # any more whitespace before the end of the code markup r"(?P=delim)", # match the exact same delimiter from the start again - re.DOTALL | re.IGNORECASE # "." also matches newlines, case insensitive + flags=re.DOTALL | re.IGNORECASE # "." also matches newlines, case insensitive ) """ Regex for formatted code, using Discord's code blocks. @@ -44,7 +45,7 @@ RAW_CODE_REGEX = re.compile( r"^(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code r"(?P<code>.*?)" # extract all the rest as code r"\s*$", # any trailing whitespace until the end of the string - re.DOTALL # "." also matches newlines + flags=re.DOTALL # "." also matches newlines ) """ Regex for raw code, *not* using Discord's code blocks. diff --git a/botcore/utils/scheduling.py b/botcore/utils/scheduling.py index 164f6b10..9517df6d 100644 --- a/botcore/utils/scheduling.py +++ b/botcore/utils/scheduling.py @@ -4,6 +4,7 @@ import asyncio import contextlib import inspect import typing +from collections import abc from datetime import datetime from functools import partial @@ -38,9 +39,9 @@ class Scheduler: self.name = name self._log = logging.get_logger(f"{__name__}.{name}") - self._scheduled_tasks: typing.Dict[typing.Hashable, asyncio.Task] = {} + self._scheduled_tasks: dict[abc.Hashable, asyncio.Task] = {} - def __contains__(self, task_id: typing.Hashable) -> bool: + def __contains__(self, task_id: abc.Hashable) -> bool: """ Return :obj:`True` if a task with the given ``task_id`` is currently scheduled. @@ -52,7 +53,7 @@ class Scheduler: """ return task_id in self._scheduled_tasks - def schedule(self, task_id: typing.Hashable, coroutine: typing.Coroutine) -> None: + def schedule(self, task_id: abc.Hashable, coroutine: abc.Coroutine) -> None: """ Schedule the execution of a ``coroutine``. @@ -79,7 +80,7 @@ class Scheduler: self._scheduled_tasks[task_id] = task self._log.debug(f"Scheduled task #{task_id} {id(task)}.") - def schedule_at(self, time: datetime, task_id: typing.Hashable, coroutine: typing.Coroutine) -> None: + def schedule_at(self, time: datetime, task_id: abc.Hashable, coroutine: abc.Coroutine) -> None: """ Schedule ``coroutine`` to be executed at the given ``time``. @@ -106,8 +107,8 @@ class Scheduler: def schedule_later( self, delay: typing.Union[int, float], - task_id: typing.Hashable, - coroutine: typing.Coroutine + task_id: abc.Hashable, + coroutine: abc.Coroutine ) -> None: """ Schedule ``coroutine`` to be executed after ``delay`` seconds. @@ -122,7 +123,7 @@ class Scheduler: """ self.schedule(task_id, self._await_later(delay, task_id, coroutine)) - def cancel(self, task_id: typing.Hashable) -> None: + def cancel(self, task_id: abc.Hashable) -> None: """ Unschedule the task identified by ``task_id``. Log a warning if the task doesn't exist. @@ -150,8 +151,8 @@ class Scheduler: async def _await_later( self, delay: typing.Union[int, float], - task_id: typing.Hashable, - coroutine: typing.Coroutine + task_id: abc.Hashable, + coroutine: abc.Coroutine ) -> None: """Await ``coroutine`` after ``delay`` seconds.""" try: @@ -173,7 +174,7 @@ class Scheduler: else: self._log.debug(f"Finally block reached for #{task_id}; {state=}") - def _task_done_callback(self, task_id: typing.Hashable, done_task: asyncio.Task) -> None: + def _task_done_callback(self, task_id: abc.Hashable, done_task: asyncio.Task) -> None: """ Delete the task and raise its exception if one exists. @@ -208,13 +209,16 @@ class Scheduler: self._log.error(f"Error in task #{task_id} {id(done_task)}!", exc_info=exception) +TASK_RETURN = typing.TypeVar("TASK_RETURN") + + def create_task( - coro: typing.Awaitable, + coro: abc.Coroutine[typing.Any, typing.Any, TASK_RETURN], *, - suppressed_exceptions: tuple[typing.Type[Exception]] = (), + suppressed_exceptions: tuple[type[Exception], ...] = (), event_loop: typing.Optional[asyncio.AbstractEventLoop] = None, **kwargs, -) -> asyncio.Task: +) -> asyncio.Task[TASK_RETURN]: """ Wrapper for creating an :obj:`asyncio.Task` which logs exceptions raised in the task. @@ -238,7 +242,7 @@ def create_task( return task -def _log_task_exception(task: asyncio.Task, *, suppressed_exceptions: typing.Tuple[typing.Type[Exception]]) -> None: +def _log_task_exception(task: asyncio.Task, *, suppressed_exceptions: tuple[type[Exception], ...]) -> None: """Retrieve and log the exception raised in ``task`` if one exists.""" with contextlib.suppress(asyncio.CancelledError): exception = task.exception() diff --git a/dev/Dockerfile b/dev/Dockerfile index 738fc51a..0b35724a 100644 --- a/dev/Dockerfile +++ b/dev/Dockerfile @@ -1,21 +1,16 @@ -FROM python:3.9-slim +FROM --platform=linux/amd64 ghcr.io/chrislovering/python-poetry-base:3.10-slim -# Set pip to have no saved cache -ENV PIP_NO_CACHE_DIR=false \ - POETRY_VIRTUALENVS_CREATE=false - -ENTRYPOINT ["/bin/bash"] -CMD ["./docker-entrypoint.sh"] - -# Install poetry -RUN pip install -U poetry - -RUN mkdir bot -WORKDIR /bot # Install project dependencies +WORKDIR /app COPY pyproject.toml poetry.lock ./ -RUN poetry install --no-dev +RUN poetry install --no-root # Copy the source code in last to optimize rebuilding the image COPY . . + +# Install again, this time with the root project +RUN poetry install + +ENTRYPOINT ["python"] +CMD ["-m", "bot"] diff --git a/dev/README.rst b/dev/README.rst new file mode 100644 index 00000000..ae4f3adc --- /dev/null +++ b/dev/README.rst @@ -0,0 +1,53 @@ +Local Development & Testing +=========================== + +To test your features locally, there are a few possible approaches: + +1. Install your local copy of botcore into a pre-existing project such as bot +2. Use the provided template from the :repo-file:`dev/bot <dev/bot>` folder + +See below for more info on both approaches. + +What's going to be common between them is you'll need to write code to test your feature. +This might mean adding new commands, modifying existing ones, changing utilities, etc. +The steps below should provide most of the groundwork you need, but the exact requirements will +vary by the feature you're working on. + + +Option 1 +-------- +1. Navigate to the project you want to install bot-core in, such as bot or sir-lancebot +2. Run ``pip install /path/to/botcore`` in the project's environment + + - The path provided to install should be the root directory of this project on your machine. + That is, the folder which contains the ``pyproject.toml`` file. + - Make sure to install in the correct environment. Most Python Discord projects use + poetry, so you can run ``poetry run pip install /path/to/botcore``. + +3. You can now use features from your local bot-core changes. + To load new changes, run the install command again. + + +Option 2 +-------- +1. Copy the :repo-file:`bot template folder <dev/bot>` to the root of the bot-core project. + This copy is going to be git-ignored, so you're free to modify it however you like. +2. Run the project + + - Locally: You can run it on your system using ``python -m bot`` + - Docker: You can run on docker using ``docker compose up -d bot``. + +3. Configure the environment variables used by the program. + You can set them in an ``.env`` file in the project root directory. The variables are: + + - ``BOT_TOKEN`` (required): Discord bot token, with all intents enabled + - ``GUILD_ID`` (required): The guild the bot should monitor + - ``PREFIX``: The prefix to use for invoking bot commands. Defaults to mentions and ``!`` + - ``ALLOWED_ROLES``: A comma seperated list of role IDs which the bot is allowed to mention + +4. You can now test your changes. You do not need to do anything to reinstall the + library if you modify your code. + +.. tip:: + The docker-compose included contains services from our other applications + to help you test out certain features. Use them as needed. diff --git a/dev/bot/__init__.py b/dev/bot/__init__.py new file mode 100644 index 00000000..71871209 --- /dev/null +++ b/dev/bot/__init__.py @@ -0,0 +1,24 @@ +import asyncio +import logging +import os +import sys + +import botcore + +if os.name == "nt": + # Change the event loop policy on Windows to avoid exceptions on exit + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + +# Some basic logging to get existing loggers to show +logging.getLogger().addHandler(logging.StreamHandler()) +logging.getLogger().setLevel(logging.DEBUG) +logging.getLogger("discord").setLevel(logging.ERROR) + + +class Bot(botcore.BotBase): + """Sample Bot implementation.""" + + async def setup_hook(self) -> None: + """Load extensions on startup.""" + await super().setup_hook() + asyncio.create_task(self.load_extensions(sys.modules[__name__])) diff --git a/dev/bot/__main__.py b/dev/bot/__main__.py new file mode 100644 index 00000000..42d212c2 --- /dev/null +++ b/dev/bot/__main__.py @@ -0,0 +1,34 @@ +import asyncio +import os + +import aiohttp +import discord +import dotenv +from discord.ext import commands + +import botcore +from . import Bot + +dotenv.load_dotenv() +botcore.utils.apply_monkey_patches() + +roles = os.getenv("ALLOWED_ROLES") +roles = [int(role) for role in roles.split(",")] if roles else [] + +bot = Bot( + guild_id=int(os.getenv("GUILD_ID")), + http_session=None, # type: ignore # We need to instantiate the session in an async context + allowed_roles=roles, + command_prefix=commands.when_mentioned_or(os.getenv("PREFIX", "!")), + intents=discord.Intents.all(), + description="Bot-core test bot.", +) + + +async def main() -> None: + """Run the bot.""" + bot.http_session = aiohttp.ClientSession() + async with bot: + await bot.start(os.getenv("BOT_TOKEN")) + +asyncio.run(main()) diff --git a/dev/bot/cog.py b/dev/bot/cog.py new file mode 100644 index 00000000..7746c54e --- /dev/null +++ b/dev/bot/cog.py @@ -0,0 +1,33 @@ +from discord.ext import commands + +from . import Bot + + +class Cog(commands.Cog): + """A simple discord.py cog.""" + + def __init__(self, _bot: Bot): + self.bot = _bot + + @commands.Cog.listener() + async def on_ready(self) -> None: + """Print a message when the client (re)connects.""" + print("Client is ready.") + + @commands.command() + async def reload(self, ctx: commands.Context) -> None: + """Reload all available cogs.""" + message = await ctx.send(":hourglass_flowing_sand: Reloading") + for ext in list(self.bot.extensions): + await self.bot.reload_extension(ext) + await message.edit(content=":white_check_mark: Done") + + @commands.command() + async def ping(self, ctx: commands.Context) -> None: + """Test if the bot is online.""" + await ctx.send("We are live!") + + +async def setup(_bot: Bot) -> None: + """Install the cog.""" + await _bot.add_cog(Cog(_bot)) diff --git a/dev/docker-compose.yaml b/dev/docker-compose.yaml deleted file mode 100644 index e1dca5bb..00000000 --- a/dev/docker-compose.yaml +++ /dev/null @@ -1,27 +0,0 @@ -version: "3.9" - -x-logging: &logging - logging: - driver: "json-file" - options: - max-file: "5" - max-size: "10m" - -x-restart-policy: &restart_policy - restart: unless-stopped - -services: - botcore: - <<: *logging - <<: *restart_policy - build: - context: . - dockerfile: Dockerfile - container_name: botcore - - volumes: - - ./logs:/bot/logs - - .:/bot:ro - - env_file: - - .env diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..af882428 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,80 @@ +# Modified version of python-discord/bot + +version: "3.8" + +x-restart-policy: &restart_policy + restart: unless-stopped + +services: + postgres: + << : *restart_policy + image: postgres:13-alpine + environment: + POSTGRES_DB: pysite + POSTGRES_PASSWORD: pysite + POSTGRES_USER: pysite + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pysite"] + interval: 2s + timeout: 1s + retries: 5 + + metricity: + restart: on-failure + depends_on: + postgres: + condition: service_healthy + image: ghcr.io/python-discord/metricity:latest + env_file: + - .env + environment: + DATABASE_URI: postgres://pysite:pysite@postgres/metricity + USE_METRICITY: ${USE_METRICITY-false} + volumes: + - .:/tmp/bot:ro + + redis: + << : *restart_policy + image: redis:5.0.9 + ports: + - "6379:6379" + + snekbox: + << : *restart_policy + image: ghcr.io/python-discord/snekbox:latest + init: true + ipc: none + ports: + - "8060:8060" + privileged: true + + web: + << : *restart_policy + image: ghcr.io/python-discord/site:latest + command: ["run", "--debug"] + ports: + - "8000:8000" + tty: true + environment: + DATABASE_URL: postgres://pysite:pysite@postgres:5432/pysite + METRICITY_DB_URL: postgres://pysite:pysite@postgres:5432/metricity + SECRET_KEY: suitable-for-development-only + STATIC_ROOT: /var/www/static + depends_on: + - metricity + + bot: + << : *restart_policy + build: + context: . + dockerfile: dev/Dockerfile + volumes: # Don't do .:/app here to ensure project venv from host doens't overwrite venv in image + - ./botcore:/app/botcore:ro + - ./bot:/app/bot:ro + tty: true + depends_on: + - web + env_file: + - .env + environment: + BOT_API_KEY: badbot13m0n8f570f942013fc818f234916ca531 diff --git a/docs/changelog.rst b/docs/changelog.rst index e666a266..3e3c7149 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,60 @@ Changelog ========= +- :release:`8.2.1 <18th September 2022>` +- :bug:`138` Bump Discord.py to :literal-url:`2.0.1 <https://discordpy.readthedocs.io/en/latest/whats_new.html#v2-0-1>`. + + +- :release:`8.2.0 <18th August 2022>` +- :support:`125` Bump Discord.py to the stable :literal-url:`2.0 release <https://discordpy.readthedocs.io/en/latest/migrating.html>`. + + +- :release:`8.1.0 <16th August 2022>` +- :support:`124` Updated :obj:`botcore.utils.regex.DISCORD_INVITE` regex to optionally match leading "http[s]" and "www". + + +- :release:`8.0.0 <27th July 2022>` +- :breaking:`110` Bump async-rediscache to v1.0.0-rc2 +- :support:`108` Bump Python version to 3.10.* +- :bug:`107 major` Declare aiodns as a project dependency. +- :support:`107` Add a sample project with boilerplate and documentation explaining how to develop for bot-core. + + +- :release:`7.5.0 <23rd July 2022>` +- :feature:`101` Add a utility to clean a string or referenced message's content + + +- :release:`7.4.0 <17th July 2022>` +- :feature:`106` Add an optional ``message`` attr to :obj:`botcore.utils.interactions.ViewWithUserAndRoleCheck`. On view timeout, this message has its view removed if set. + + +- :release:`7.3.1 <16th July 2022>` +- :bug:`104` Fix :obj:`botcore.utils.interactions.DeleteMessageButton` not working due to using wrong delete method. + + +- :release:`7.3.0 <16th July 2022>` +- :feature:`103` Add a generic view :obj:`botcore.utils.interactions.ViewWithUserAndRoleCheck` that only allows specified users and roles to interaction with it +- :feature:`103` Add a button :obj:`botcore.utils.interactions.DeleteMessageButton` that deletes the message attached to its parent view. + + +- :release:`7.2.2 <9th July 2022>` +- :bug:`98` Only close ``BotBase.stats._transport`` if ``BotBase.stats`` was created + + +- :release:`7.2.1 <30th June 2022>` +- :bug:`96` Fix attempts to connect to ``BotBase.statsd_url`` when it is None. +- :bug:`91` Fix incorrect docstring for ``botcore.utils.member.handle_role_change``. +- :bug:`91` Pass missing self parameter to ``BotBase.ping_services``. +- :bug:`91` Add missing await to ``BotBase.ping_services`` in some cases. + + +- :release:`7.2.0 <28th June 2022>` +- :support:`93` Bump Discord.py to :literal-url:`0eb3d26 <https://github.com/Rapptz/discord.py/commit/0eb3d26343969a25ffc43ba72eca42538d2e7e7a>`: + + - Adds support for auto mod, of which the new auto_mod MESSAGE_TYPE is needed for our filter system. + + +- :release:`7.1.3 <30th May 2022>` 79 - :support:`79` Add `sphinx-multiversion <https://pypi.org/project/sphinx-multiversion/>`_ to make available older doc versions. - :support:`79` Restore on-site changelog. diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 00000000..25b8e0a7 --- /dev/null +++ b/docs/development.rst @@ -0,0 +1,2 @@ +.. Stub file to expose the README to sphinx +.. include:: ../dev/README.rst diff --git a/docs/index.rst b/docs/index.rst index 0a375b90..aee7b269 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ Reference :caption: Other: :hidden: + development changelog @@ -26,4 +27,5 @@ Extras * :ref:`genindex` * :ref:`search` * :repo-file:`Information <docs/README.md>` +* :doc:`development` * :doc:`changelog` diff --git a/docs/utils.py b/docs/utils.py index 9d299ebf..c8bbc895 100644 --- a/docs/utils.py +++ b/docs/utils.py @@ -64,6 +64,7 @@ def linkcode_resolve(repo_link: str, domain: str, info: dict[str, str]) -> typin try: lines, start = inspect.getsourcelines(symbol[-1]) + module = inspect.getmodule(symbol[-1]) end = start + len(lines) except TypeError: # Find variables by parsing the ast diff --git a/poetry.lock b/poetry.lock index 236f3e0f..a3e1b8b6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,15 @@ [[package]] +name = "aiodns" +version = "3.0.0" +description = "Simple DNS resolver for asyncio" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycares = ">=4.0.0" + +[[package]] name = "aiohttp" version = "3.8.1" description = "Async http client/server framework (asyncio)" @@ -19,21 +30,6 @@ yarl = ">=1.0,<2.0" speedups = ["aiodns", "brotli", "cchardet"] [[package]] -name = "aioredis" -version = "2.0.1" -description = "asyncio (PEP 3156) Redis support" -category = "main" -optional = true -python-versions = ">=3.6" - -[package.dependencies] -async-timeout = "*" -typing-extensions = "*" - -[package.extras] -hiredis = ["hiredis (>=1.0)"] - -[[package]] name = "aiosignal" version = "1.2.0" description = "aiosignal: a list of registered asynchronous callbacks" @@ -54,18 +50,18 @@ python-versions = "*" [[package]] name = "async-rediscache" -version = "0.2.0" +version = "1.0.0rc2" description = "An easy to use asynchronous Redis cache" category = "main" optional = true python-versions = "~=3.7" [package.dependencies] -aioredis = ">=1" -fakeredis = {version = ">=1.4.4", extras = ["lua"], optional = true, markers = "extra == \"fakeredis\""} +fakeredis = {version = ">=1.7.1", extras = ["lua"], optional = true, markers = "extra == \"fakeredis\""} +redis = ">=4.2,<5.0" [package.extras] -fakeredis = ["fakeredis[lua] (>=1.4.4)"] +fakeredis = ["fakeredis[lua] (>=1.7.1)"] [[package]] name = "async-timeout" @@ -76,14 +72,6 @@ optional = false python-versions = ">=3.6" [[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] name = "attrs" version = "21.4.0" description = "Classes Without Boilerplate" @@ -132,6 +120,17 @@ optional = false python-versions = ">=3.6" [[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] name = "cfgv" version = "3.3.1" description = "Validate configuration and produce human readable error messages." @@ -160,7 +159,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.1" +version = "6.4.4" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -188,7 +187,7 @@ dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "im [[package]] name = "discord.py" -version = "2.0.0a0" +version = "2.0.1" description = "A Python wrapper for the Discord API" category = "main" optional = false @@ -198,14 +197,10 @@ python-versions = ">=3.8.0" aiohttp = ">=3.7.4,<4" [package.extras] -docs = ["sphinx (==4.4.0)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport", "typing-extensions"] -speed = ["orjson (>=3.5.4)", "aiodns (>=1.1)", "brotli", "cchardet"] -test = ["coverage", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock"] voice = ["PyNaCl (>=1.3.0,<1.6)"] - -[package.source] -type = "url" -url = "https://github.com/Rapptz/discord.py/archive/4cbe8f58e16f6a76371ce45a69e0832130d6d24f.zip" +test = ["typing-extensions (>=4.3,<5)", "pytest-mock", "pytest-cov", "pytest-asyncio", "pytest", "coverage"] +speed = ["cchardet (==2.1.7)", "brotli", "aiodns (>=1.1)", "orjson (>=3.5.4)"] +docs = ["typing-extensions (>=4.3,<5)", "sphinxcontrib-websupport", "sphinxcontrib-trio (==1.1.2)", "sphinx (==4.4.0)"] [[package]] name = "distlib" @@ -236,7 +231,7 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.8.1" +version = "1.9.1" description = "Fake implementation of redis API for testing purposes." category = "main" optional = true @@ -266,32 +261,32 @@ testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-co [[package]] name = "flake8" -version = "4.0.1" +version = "5.0.4" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.6.1" [package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" [[package]] name = "flake8-annotations" -version = "2.9.0" +version = "2.9.1" description = "Flake8 Type Annotation Checks" category = "dev" optional = false python-versions = ">=3.7,<4.0" [package.dependencies] -attrs = ">=21.4,<22.0" +attrs = ">=21.4" flake8 = ">=3.7" [[package]] name = "flake8-bugbear" -version = "22.4.25" +version = "22.9.11" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." category = "dev" optional = false @@ -328,17 +323,6 @@ python-versions = "*" pycodestyle = "*" [[package]] -name = "flake8-polyfill" -version = "1.0.2" -description = "Polyfill package for Flake8 plugins" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -flake8 = "*" - -[[package]] name = "flake8-string-format" version = "0.3.0" description = "string format checker, plugin for flake8" @@ -381,16 +365,17 @@ python-versions = ">=3.7" [[package]] name = "furo" -version = "2022.4.7" +version = "2022.9.15" description = "A clean customisable Sphinx documentation theme." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] beautifulsoup4 = "*" -pygments = ">=2.7,<3.0" -sphinx = ">=4.0,<5.0" +pygments = ">=2.7" +sphinx = ">=4.0,<6.0" +sphinx-basic-ng = "*" [[package]] name = "gitdb" @@ -442,22 +427,6 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "importlib-metadata" -version = "4.11.4" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] - -[[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" @@ -497,11 +466,11 @@ python-versions = ">=3.7" [[package]] name = "mccabe" -version = "0.6.1" +version = "0.7.0" description = "McCabe checker, plugin for flake8" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "mslex" @@ -540,15 +509,14 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pep8-naming" -version = "0.12.1" +version = "0.13.2" description = "Check PEP-8 naming conventions, plugin for flake8" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [package.dependencies] flake8 = ">=3.9.1" -flake8-polyfill = ">=1.0.2,<2" [[package]] name = "platformdirs" @@ -576,7 +544,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.19.0" +version = "2.20.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -610,12 +578,34 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] +name = "pycares" +version = "4.2.2" +description = "Python interface for c-ares" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cffi = ">=1.5.0" + +[package.extras] +idna = ["idna (>=2.1)"] + +[[package]] name = "pycodestyle" -version = "2.8.0" +version = "2.9.1" description = "Python style guide checker" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pydocstyle" @@ -633,11 +623,11 @@ toml = ["toml"] [[package]] name = "pyflakes" -version = "2.4.0" +version = "2.5.0" description = "passive checker of Python programs" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [[package]] name = "pygments" @@ -660,14 +650,13 @@ diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pytest" -version = "7.1.2" +version = "7.1.3" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" @@ -726,11 +715,11 @@ testing = ["filelock"] [[package]] name = "python-dotenv" -version = "0.20.0" +version = "0.21.0" description = "Read key-value pairs from a .env file and set them as environment variables" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [package.extras] cli = ["click (>=5.0)"] @@ -848,7 +837,7 @@ python-versions = ">=3.6" [[package]] name = "sphinx" -version = "4.5.0" +version = "5.1.1" description = "Python documentation generator" category = "dev" optional = false @@ -858,9 +847,8 @@ python-versions = ">=3.6" alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.18" +docutils = ">=0.14,<0.20" imagesize = "*" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" packaging = "*" Pygments = ">=2.0" @@ -875,23 +863,37 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"] -test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"] +lint = ["flake8 (>=3.5.0)", "flake8-comprehensions", "flake8-bugbear", "isort", "mypy (>=0.971)", "sphinx-lint", "docutils-stubs", "types-typed-ast", "types-requests"] +test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-autodoc-typehints" -version = "1.18.1" +version = "1.19.2" description = "Type hints (PEP 484) support for the Sphinx autodoc extension" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -Sphinx = ">=4.5" +Sphinx = ">=5.1.1" [package.extras] -testing = ["covdefaults (>=2.2)", "coverage (>=6.3)", "diff-cover (>=6.4)", "nptyping (>=2)", "pytest (>=7.1)", "pytest-cov (>=3)", "sphobjinv (>=2)", "typing-extensions (>=4.1)"] -type_comments = ["typed-ast (>=1.5.2)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "diff-cover (>=6.5.1)", "nptyping (>=2.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "sphobjinv (>=2.2.2)", "typing-extensions (>=4.3)"] +type_comments = ["typed-ast (>=1.5.4)"] + +[[package]] +name = "sphinx-basic-ng" +version = "0.0.1a12" +description = "A modern skeleton for Sphinx themes." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +sphinx = ">=4.0,<6.0" + +[package.extras] +docs = ["furo", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs", "ipython"] [[package]] name = "sphinx-multiversion" @@ -985,7 +987,7 @@ python-versions = "*" [[package]] name = "taskipy" -version = "1.10.2" +version = "1.10.3" description = "tasks runner for python projects" category = "dev" optional = false @@ -1015,9 +1017,9 @@ python-versions = ">=3.7" [[package]] name = "typing-extensions" -version = "4.2.0" +version = "4.3.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" @@ -1072,27 +1074,19 @@ python-versions = ">=3.6" idna = ">=2.0" multidict = ">=4.0" -[[package]] -name = "zipp" -version = "3.8.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] - [extras] async-rediscache = ["async-rediscache"] [metadata] lock-version = "1.1" -python-versions = "3.9.*" -content-hash = "fa1b3253f14aee762b0ca0e5e719c4b1a61d35fae23c51f74a56c728741d8f3c" +python-versions = "3.10.*" +content-hash = "02800700a2a02f4a842b4172f6d715da119bf7b3b4144bdb23a5cb50bc6d2364" [metadata.files] +aiodns = [ + {file = "aiodns-3.0.0-py3-none-any.whl", hash = "sha256:2b19bc5f97e5c936638d28e665923c093d8af2bf3aa88d35c43417fa25d136a2"}, + {file = "aiodns-3.0.0.tar.gz", hash = "sha256:946bdfabe743fceeeb093c8a010f5d1645f708a241be849e17edfb0e49e08cd6"}, +] aiohttp = [ {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, @@ -1167,10 +1161,6 @@ aiohttp = [ {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, ] -aioredis = [ - {file = "aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6"}, - {file = "aioredis-2.0.1.tar.gz", hash = "sha256:eaa51aaf993f2d71f54b70527c440437ba65340588afeb786cd87c55c89cd98e"}, -] aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, @@ -1179,18 +1169,11 @@ alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] -async-rediscache = [ - {file = "async-rediscache-0.2.0.tar.gz", hash = "sha256:c1fd95fe530211b999748ebff96e2e9b629f2664957f9b36916b898e42fc57c4"}, - {file = "async_rediscache-0.2.0-py3-none-any.whl", hash = "sha256:710676211b407399c9ad94afa66fa04c22a936be11ba6f227e6c74cfa140ce78"}, -] +async-rediscache = [] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, @@ -1207,6 +1190,7 @@ certifi = [ {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, ] +cffi = [] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, @@ -1219,49 +1203,7 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] -coverage = [ - {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, - {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, - {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, - {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, - {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, - {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, - {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, - {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, - {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, - {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, - {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, - {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, - {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, -] +coverage = [] deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, @@ -1279,26 +1221,14 @@ execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] -fakeredis = [ - {file = "fakeredis-1.8.1-py3-none-any.whl", hash = "sha256:4a0f8fe0d5c18147864db50ae2e86f667420ea06653bec08b3a5fccfd3fbde6f"}, - {file = "fakeredis-1.8.1.tar.gz", hash = "sha256:ca516f86181f85615cd8210854b43acbe7b1f37ed8a082c5557749c73f2f0dd3"}, -] +fakeredis = [] filelock = [ {file = "filelock-3.7.0-py3-none-any.whl", hash = "sha256:c7b5fdb219b398a5b28c8e4c1893ef5f98ece6a38c6ab2c22e26ec161556fed6"}, {file = "filelock-3.7.0.tar.gz", hash = "sha256:b795f1b42a61bbf8ec7113c341dad679d772567b936fbd1bf43c9a238e673e20"}, ] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -flake8-annotations = [ - {file = "flake8-annotations-2.9.0.tar.gz", hash = "sha256:63fb3f538970b6a8dfd84125cf5af16f7b22e52d5032acb3b7eb23645ecbda9b"}, - {file = "flake8_annotations-2.9.0-py3-none-any.whl", hash = "sha256:84f46de2964cb18fccea968d9eafce7cf857e34d913d515120795b9af6498d56"}, -] -flake8-bugbear = [ - {file = "flake8-bugbear-22.4.25.tar.gz", hash = "sha256:f7c080563fca75ee6b205d06b181ecba22b802babb96b0b084cc7743d6908a55"}, - {file = "flake8_bugbear-22.4.25-py3-none-any.whl", hash = "sha256:ec374101cddf65bd7a96d393847d74e58d3b98669dbf9768344c39b6290e8bd6"}, -] +flake8 = [] +flake8-annotations = [] +flake8-bugbear = [] flake8-docstrings = [ {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, @@ -1307,10 +1237,6 @@ flake8-import-order = [ {file = "flake8-import-order-0.18.1.tar.gz", hash = "sha256:a28dc39545ea4606c1ac3c24e9d05c849c6e5444a50fb7e9cdd430fc94de6e92"}, {file = "flake8_import_order-0.18.1-py2.py3-none-any.whl", hash = "sha256:90a80e46886259b9c396b578d75c749801a41ee969a235e163cfe1be7afd2543"}, ] -flake8-polyfill = [ - {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"}, - {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"}, -] flake8-string-format = [ {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, @@ -1383,10 +1309,7 @@ frozenlist = [ {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, ] -furo = [ - {file = "furo-2022.4.7-py3-none-any.whl", hash = "sha256:7f3e3d2fb977483590f8ecb2c2cd511bd82661b79c18efb24de9558bc9cdf2d7"}, - {file = "furo-2022.4.7.tar.gz", hash = "sha256:96204ab7cd047e4b6c523996e0279c4c629a8fc31f4f109b2efd470c17f49c80"}, -] +furo = [] gitdb = [ {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, @@ -1407,10 +1330,6 @@ imagesize = [ {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, ] -importlib-metadata = [ - {file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"}, - {file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"}, -] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1529,10 +1448,7 @@ markupsafe = [ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] +mccabe = [] mslex = [ {file = "mslex-0.3.0-py2.py3-none-any.whl", hash = "sha256:380cb14abf8fabf40e56df5c8b21a6d533dc5cbdcfe42406bbf08dda8f42e42a"}, {file = "mslex-0.3.0.tar.gz", hash = "sha256:4a1ac3f25025cad78ad2fe499dd16d42759f7a3801645399cce5c404415daa97"}, @@ -1606,10 +1522,7 @@ packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] -pep8-naming = [ - {file = "pep8-naming-0.12.1.tar.gz", hash = "sha256:bb2455947757d162aa4cad55dba4ce029005cd1692f2899a21d51d8630ca7841"}, - {file = "pep8_naming-0.12.1-py2.py3-none-any.whl", hash = "sha256:4a8daeaeb33cfcde779309fc0c9c0a68a3bbe2ad8a8308b763c5068f86eb9f37"}, -] +pep8-naming = [] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, @@ -1618,10 +1531,7 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -pre-commit = [ - {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, - {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, -] +pre-commit = [] psutil = [ {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:799759d809c31aab5fe4579e50addf84565e71c1dc9f1c31258f159ff70d3f87"}, {file = "psutil-5.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9272167b5f5fbfe16945be3db475b3ce8d792386907e673a209da686176552af"}, @@ -1660,18 +1570,17 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, +pycares = [] +pycodestyle = [] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] +pyflakes = [] pygments = [ {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, @@ -1680,10 +1589,7 @@ pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] -pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, -] +pytest = [] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, @@ -1696,10 +1602,7 @@ pytest-xdist = [ {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, ] -python-dotenv = [ - {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, - {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, -] +python-dotenv = [] pytz = [ {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, @@ -1775,14 +1678,9 @@ soupsieve = [ {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, ] -sphinx = [ - {file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"}, - {file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"}, -] -sphinx-autodoc-typehints = [ - {file = "sphinx_autodoc_typehints-1.18.1-py3-none-any.whl", hash = "sha256:f8f5bb7c13a9a71537dc2be2eb3b9e28a9711e2454df63587005eacf6fbac453"}, - {file = "sphinx_autodoc_typehints-1.18.1.tar.gz", hash = "sha256:07631c5f0c6641e5ba27143494aefc657e029bed3982138d659250e617f6f929"}, -] +sphinx = [] +sphinx-autodoc-typehints = [] +sphinx-basic-ng = [] sphinx-multiversion = [ {file = "sphinx-multiversion-0.2.4.tar.gz", hash = "sha256:5cd1ca9ecb5eed63cb8d6ce5e9c438ca13af4fa98e7eb6f376be541dd4990bcb"}, {file = "sphinx_multiversion-0.2.4-py3-none-any.whl", hash = "sha256:dec29f2a5890ad68157a790112edc0eb63140e70f9df0a363743c6258fbeb478"}, @@ -1815,10 +1713,7 @@ statsd = [ {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, {file = "statsd-3.3.0.tar.gz", hash = "sha256:e3e6db4c246f7c59003e51c9720a51a7f39a396541cb9b147ff4b14d15b5dd1f"}, ] -taskipy = [ - {file = "taskipy-1.10.2-py3-none-any.whl", hash = "sha256:58d5382d90d5dd94ca8c612855377e5a98b9cb669c208ebb55d6a45946de3f9b"}, - {file = "taskipy-1.10.2.tar.gz", hash = "sha256:eae4feb74909da3ad0ca0275802e1c2f56048612529bd763feb922d284d8a253"}, -] +taskipy = [] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1827,10 +1722,7 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -typing-extensions = [ - {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, - {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, -] +typing-extensions = [] urllib3 = [ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, @@ -1979,7 +1871,3 @@ yarl = [ {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, ] -zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] diff --git a/pyproject.toml b/pyproject.toml index b6ab4a23..6386c6ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bot-core" -version = "7.1.1" +version = "8.2.1" description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." authors = ["Python Discord <[email protected]>"] license = "MIT" @@ -15,35 +15,36 @@ packages = [ exclude = ["tests", "tests.*"] [tool.poetry.dependencies] -python = "3.9.*" -"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/4cbe8f58e16f6a76371ce45a69e0832130d6d24f.zip"} -async-rediscache = { version = "0.2.0", extras = ["fakeredis"], optional = true } +python = "3.10.*" +"discord.py" = "2.0.1" +async-rediscache = { version = "1.0.0rc2", extras = ["fakeredis"], optional = true } statsd = "3.3.0" +aiodns = "3.0.0" [tool.poetry.extras] async-rediscache = ["async-rediscache"] [tool.poetry.dev-dependencies] -typing-extensions = "4.2.0" -flake8 = "4.0.1" -flake8-annotations = "2.9.0" -flake8-bugbear = "22.4.25" +typing-extensions = "4.3.0" +flake8 = "5.0.4" +flake8-annotations = "2.9.1" +flake8-bugbear = "22.9.11" flake8-docstrings = "1.6.0" flake8-import-order = "0.18.1" flake8-string-format = "0.3.0" flake8-tidy-imports = "4.8.0" flake8-todo = "0.7" -pep8-naming = "0.12.1" -pre-commit = "2.19.0" -taskipy = "1.10.2" -python-dotenv = "0.20.0" -pytest = "7.1.2" +pep8-naming = "0.13.2" +pre-commit = "2.20.0" +taskipy = "1.10.3" +python-dotenv = "0.21.0" +pytest = "7.1.3" pytest-cov = "3.0.0" pytest-xdist = "2.5.0" -Sphinx = "4.5.0" +Sphinx = "5.1.1" GitPython = "3.1.27" -sphinx-autodoc-typehints = "1.18.1" -furo = "2022.4.7" +sphinx-autodoc-typehints = "1.19.2" +furo = "2022.9.15" releases = "1.6.3" sphinx-multiversion = "0.2.4" diff --git a/tests/botcore/utils/test_regex.py b/tests/botcore/utils/test_regex.py index 2ffd0e46..491e22bd 100644 --- a/tests/botcore/utils/test_regex.py +++ b/tests/botcore/utils/test_regex.py @@ -4,8 +4,18 @@ from typing import Optional from botcore.utils.regex import DISCORD_INVITE -def use_regex(s: str) -> Optional[str]: - """Helper function to run the Regex on a string. +def match_regex(s: str) -> Optional[str]: + """Helper function to run re.match on a string. + + Return the invite capture group, if the string matches the pattern + else return None + """ + result = DISCORD_INVITE.match(s) + return result if result is None else result.group("invite") + + +def search_regex(s: str) -> Optional[str]: + """Helper function to run re.search on a string. Return the invite capture group, if the string matches the pattern else return None @@ -19,32 +29,37 @@ class UtilsRegexTests(unittest.TestCase): def test_discord_invite_positives(self): """Test the DISCORD_INVITE regex on a set of strings we would expect to capture.""" - self.assertEqual(use_regex("discord.gg/python"), "python") - self.assertEqual(use_regex("https://discord.gg/python"), "python") - self.assertEqual(use_regex("discord.com/invite/python"), "python") - self.assertEqual(use_regex("discordapp.com/invite/python"), "python") - self.assertEqual(use_regex("discord.me/python"), "python") - self.assertEqual(use_regex("discord.li/python"), "python") - self.assertEqual(use_regex("discord.io/python"), "python") - self.assertEqual(use_regex(".gg/python"), "python") - - self.assertEqual(use_regex("discord.gg/python/but/extra"), "python/but/extra") - self.assertEqual(use_regex("discord.me/this/isnt/python"), "this/isnt/python") - self.assertEqual(use_regex(".gg/a/a/a/a/a/a/a/a/a/a/a"), "a/a/a/a/a/a/a/a/a/a/a") - self.assertEqual(use_regex("discordapp.com/invite/python/snakescord"), "python/snakescord") - self.assertEqual(use_regex("http://discord.gg/python/%20/notpython"), "python/%20/notpython") - self.assertEqual(use_regex("discord.gg/python?=ts/notpython"), "python?=ts/notpython") - self.assertEqual(use_regex("https://discord.gg/python#fragment/notpython"), "python#fragment/notpython") - self.assertEqual(use_regex("https://discord.gg/python/~/notpython"), "python/~/notpython") - - self.assertEqual(use_regex("https://discord.gg/python with whitespace"), "python") - self.assertEqual(use_regex(" https://discord.gg/python "), "python") + self.assertEqual(match_regex("discord.gg/python"), "python") + self.assertEqual(match_regex("https://discord.gg/python"), "python") + self.assertEqual(match_regex("https://www.discord.gg/python"), "python") + self.assertEqual(match_regex("discord.com/invite/python"), "python") + self.assertEqual(match_regex("www.discord.com/invite/python"), "python") + self.assertEqual(match_regex("discordapp.com/invite/python"), "python") + self.assertEqual(match_regex("discord.me/python"), "python") + self.assertEqual(match_regex("discord.li/python"), "python") + self.assertEqual(match_regex("discord.io/python"), "python") + self.assertEqual(match_regex(".gg/python"), "python") + + self.assertEqual(match_regex("discord.gg/python/but/extra"), "python/but/extra") + self.assertEqual(match_regex("discord.me/this/isnt/python"), "this/isnt/python") + self.assertEqual(match_regex(".gg/a/a/a/a/a/a/a/a/a/a/a"), "a/a/a/a/a/a/a/a/a/a/a") + self.assertEqual(match_regex("discordapp.com/invite/python/snakescord"), "python/snakescord") + self.assertEqual(match_regex("http://discord.gg/python/%20/notpython"), "python/%20/notpython") + self.assertEqual(match_regex("discord.gg/python?=ts/notpython"), "python?=ts/notpython") + self.assertEqual(match_regex("https://discord.gg/python#fragment/notpython"), "python#fragment/notpython") + self.assertEqual(match_regex("https://discord.gg/python/~/notpython"), "python/~/notpython") + + self.assertEqual(search_regex("https://discord.gg/python with whitespace"), "python") + self.assertEqual(search_regex(" https://discord.gg/python "), "python") def test_discord_invite_negatives(self): """Test the DISCORD_INVITE regex on a set of strings we would expect to not capture.""" - self.assertEqual(use_regex("another string"), None) - self.assertEqual(use_regex("https://pythondiscord.com"), None) - self.assertEqual(use_regex("https://discord.com"), None) - self.assertEqual(use_regex("https://discord.gg"), None) - self.assertEqual(use_regex("https://discord.gg/ python"), None) + self.assertEqual(match_regex("another string"), None) + self.assertEqual(match_regex("https://pythondiscord.com"), None) + self.assertEqual(match_regex("https://discord.com"), None) + self.assertEqual(match_regex("https://discord.gg"), None) + self.assertEqual(match_regex("https://discord.gg/ python"), None) + + self.assertEqual(search_regex("https://discord.com with whitespace"), None) + self.assertEqual(search_regex(" https://discord.com "), None) @@ -3,7 +3,7 @@ max-line-length=120 docstring-convention=all import-order-style=pycharm application_import_names=botcore,docs,tests -exclude=.cache,.venv,.git,constants.py +exclude=.cache,.venv,.git,constants.py,bot/ ignore= B311,W503,E226,S311,T000,E731 # Missing Docstrings |