diff --git a/.github/workflows/docs-localization-download.yml b/.github/workflows/docs-localization-download.yml index cfe110c858..cd240fd3a9 100644 --- a/.github/workflows/docs-localization-download.yml +++ b/.github/workflows/docs-localization-download.yml @@ -40,7 +40,7 @@ jobs: working-directory: ./docs - name: "Crowdin" id: crowdin - uses: crowdin/github-action@v2.7.0 + uses: crowdin/github-action@v2.7.1 with: upload_sources: false upload_translations: false diff --git a/.github/workflows/docs-localization-upload.yml b/.github/workflows/docs-localization-upload.yml index a0a2213c48..113f42214a 100644 --- a/.github/workflows/docs-localization-upload.yml +++ b/.github/workflows/docs-localization-upload.yml @@ -44,7 +44,7 @@ jobs: sphinx-intl update -p ./build/locales ${{ vars.SPHINX_LANGUAGES }} working-directory: ./docs - name: "Crowdin" - uses: crowdin/github-action@v2.7.0 + uses: crowdin/github-action@v2.7.1 with: upload_sources: true upload_translations: false diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f53f7cf96b..9521a283d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: # - --remove-duplicate-keys # - --remove-unused-variables - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade exclude: \.(po|pot|yml|yaml)$ diff --git a/CHANGELOG.md b/CHANGELOG.md index b31e2a492a..fdfdc6d896 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,8 +53,10 @@ These changes are available on the `master` branch, but have not yet been releas ([#2598](https://github.com/Pycord-Development/pycord/pull/2598)) - Added the ability to change the API's base URL with `Route.API_BASE_URL`. ([#2714](https://github.com/Pycord-Development/pycord/pull/2714)) -- Added the ability to pass a `datetime.time` object to `format_dt` +- Added the ability to pass a `datetime.time` object to `format_dt`. ([#2747](https://github.com/Pycord-Development/pycord/pull/2747)) +- Added `discord.Interaction.created_at`. + ([#2801](https://github.com/Pycord-Development/pycord/pull/2801)) ### Fixed @@ -105,6 +107,10 @@ These changes are available on the `master` branch, but have not yet been releas ([#2739](https://github.com/Pycord-Development/pycord/pull/2739)) - Fixed missing `None` type hints in `Select.__init__`. ([#2746](https://github.com/Pycord-Development/pycord/pull/2746)) +- Fixed `TypeError` when using `Flag` with Python 3.11+. + ([#2759](https://github.com/Pycord-Development/pycord/pull/2759)) +- Fixed `TypeError` when specifying `thread_name` in `Webhook.send`. + ([#2761](https://github.com/Pycord-Development/pycord/pull/2761)) - Updated `valid_locales` to support `in` and `es-419`. ([#2767](https://github.com/Pycord-Development/pycord/pull/2767)) - Fixed `Webhook.edit` not working with `attachments=[]`. @@ -142,6 +148,11 @@ These changes are available on the `master` branch, but have not yet been releas - Deprecated `Interaction.cached_channel` in favor of `Interaction.channel`. ([#2658](https://github.com/Pycord-Development/pycord/pull/2658)) +### Removed + +- Removed deprecated support for `Option` in `BridgeCommand`. Use `BridgeOption` + instead. ([#2731])(https://github.com/Pycord-Development/pycord/pull/2731)) + ## [2.6.1] - 2024-09-15 ### Fixed diff --git a/discord/ext/bridge/core.py b/discord/ext/bridge/core.py index 9dc58fb009..fd1e1dd867 100644 --- a/discord/ext/bridge/core.py +++ b/discord/ext/bridge/core.py @@ -98,26 +98,12 @@ class BridgeExtCommand(Command): def __init__(self, func, **kwargs): super().__init__(func, **kwargs) - # TODO: v2.7: Remove backwards support for Option in bridge commands. - for name, option in self.params.items(): + for option in self.params.values(): if isinstance(option.annotation, Option) and not isinstance( option.annotation, BridgeOption ): - # Warn not to do this - warn_deprecated( - "Using Option for bridge commands", - "BridgeOption", - "2.5", - "2.7", - reference="https://github.com/Pycord-Development/pycord/pull/2417", - stacklevel=6, - ) - # Override the convert method of the parameter's annotated Option. - # We can use the convert method from BridgeOption, and bind "self" - # using a manual invocation of the descriptor protocol. - # Definitely not a good approach, but gets the job done until removal. - self.params[name].annotation.convert = BridgeOption.convert.__get__( - self.params[name].annotation + raise TypeError( + f"{option.annotation.__class__.__name__} is not supported in bridge commands. Use BridgeOption instead." ) async def dispatch_error(self, ctx: BridgeExtContext, error: Exception) -> None: diff --git a/discord/ext/commands/flags.py b/discord/ext/commands/flags.py index 54e7e0c37c..ebe54ab5fd 100644 --- a/discord/ext/commands/flags.py +++ b/discord/ext/commands/flags.py @@ -31,12 +31,11 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Iterator, Literal, Pattern, TypeVar, Union -from discord.utils import MISSING, MissingField, maybe_coroutine, resolve_annotation - -if sys.version_info >= (3, 11): - _MISSING = MissingField -else: - _MISSING = MISSING +from discord.utils import ( + MISSING, + maybe_coroutine, + resolve_annotation, +) from .converter import run_converters from .errors import ( @@ -59,6 +58,10 @@ from .context import Context +def _missing_field_factory() -> field: + return field(default_factory=lambda: MISSING) + + @dataclass class Flag: """Represents a flag parameter for :class:`FlagConverter`. @@ -86,13 +89,13 @@ class Flag: Whether multiple given values overrides the previous value. """ - name: str = _MISSING + name: str = _missing_field_factory() aliases: list[str] = field(default_factory=list) - attribute: str = _MISSING - annotation: Any = _MISSING - default: Any = _MISSING - max_args: int = _MISSING - override: bool = _MISSING + attribute: str = _missing_field_factory() + annotation: Any = _missing_field_factory() + default: Any = _missing_field_factory() + max_args: int = _missing_field_factory() + override: bool = _missing_field_factory() cast_to_dict: bool = False @property diff --git a/discord/guild.py b/discord/guild.py index 337abd31c0..6a9d54537a 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -88,7 +88,7 @@ from .welcome_screen import WelcomeScreen, WelcomeScreenChannel from .widget import Widget -__all__ = ("Guild",) +__all__ = ("BanEntry", "Guild") MISSING = utils.MISSING diff --git a/discord/interactions.py b/discord/interactions.py index 57628f4691..0834e9bb75 100644 --- a/discord/interactions.py +++ b/discord/interactions.py @@ -26,6 +26,7 @@ from __future__ import annotations import asyncio +import datetime from typing import TYPE_CHECKING, Any, Coroutine, Union from . import utils @@ -300,6 +301,11 @@ def guild(self) -> Guild | None: return self._guild return self._state and self._state._get_guild(self.guild_id) + @property + def created_at(self) -> datetime.datetime: + """Returns the interaction's creation time in UTC.""" + return utils.snowflake_time(self.id) + def is_command(self) -> bool: """Indicates whether the interaction is an application command.""" return self.type == InteractionType.application_command diff --git a/discord/ui/view.py b/discord/ui/view.py index c54cb58f13..bbfa353478 100644 --- a/discord/ui/view.py +++ b/discord/ui/view.py @@ -32,7 +32,7 @@ import traceback from functools import partial from itertools import groupby -from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterator, Sequence +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterator, Sequence, TypeVar from ..components import ActionRow as ActionRowComponent from ..components import Button as ButtonComponent @@ -51,6 +51,8 @@ from ..state import ConnectionState from ..types.components import Component as ComponentPayload +V = TypeVar("V", bound="View", covariant=True) + def _walk_all_components(components: list[Component]) -> Iterator[Component]: for item in components: @@ -60,7 +62,7 @@ def _walk_all_components(components: list[Component]) -> Iterator[Component]: yield item -def _component_to_item(component: Component) -> Item: +def _component_to_item(component: Component) -> Item[V]: if isinstance(component, ButtonComponent): from .button import Button @@ -75,7 +77,7 @@ def _component_to_item(component: Component) -> Item: class _ViewWeights: __slots__ = ("weights",) - def __init__(self, children: list[Item]): + def __init__(self, children: list[Item[V]]): self.weights: list[int] = [0, 0, 0, 0, 0] key = lambda i: sys.maxsize if i.row is None else i.row @@ -84,14 +86,14 @@ def __init__(self, children: list[Item]): for item in group: self.add_item(item) - def find_open_space(self, item: Item) -> int: + def find_open_space(self, item: Item[V]) -> int: for index, weight in enumerate(self.weights): if weight + item.width <= 5: return index raise ValueError("could not find open space for item") - def add_item(self, item: Item) -> None: + def add_item(self, item: Item[V]) -> None: if item.row is not None: total = self.weights[item.row] + item.width if total > 5: @@ -105,7 +107,7 @@ def add_item(self, item: Item) -> None: self.weights[index] += item.width item._rendered_row = index - def remove_item(self, item: Item) -> None: + def remove_item(self, item: Item[V]) -> None: if item._rendered_row is not None: self.weights[item._rendered_row] -= item.width item._rendered_row = None @@ -163,15 +165,15 @@ def __init_subclass__(cls) -> None: def __init__( self, - *items: Item, + *items: Item[V], timeout: float | None = 180.0, disable_on_timeout: bool = False, ): self.timeout = timeout self.disable_on_timeout = disable_on_timeout - self.children: list[Item] = [] + self.children: list[Item[V]] = [] for func in self.__view_children_items__: - item: Item = func.__discord_ui_model_type__( + item: Item[V] = func.__discord_ui_model_type__( **func.__discord_ui_model_kwargs__ ) item.callback = partial(func, self, item) @@ -213,7 +215,7 @@ async def __timeout_task_impl(self) -> None: await asyncio.sleep(self.__timeout_expiry - now) def to_components(self) -> list[dict[str, Any]]: - def key(item: Item) -> int: + def key(item: Item[V]) -> int: return item._rendered_row or 0 children = sorted(self.children, key=key) @@ -267,7 +269,7 @@ def _expires_at(self) -> float | None: return time.monotonic() + self.timeout return None - def add_item(self, item: Item) -> None: + def add_item(self, item: Item[V]) -> None: """Adds an item to the view. Parameters @@ -295,7 +297,7 @@ def add_item(self, item: Item) -> None: item._view = self self.children.append(item) - def remove_item(self, item: Item) -> None: + def remove_item(self, item: Item[V]) -> None: """Removes an item from the view. Parameters @@ -316,7 +318,7 @@ def clear_items(self) -> None: self.children.clear() self.__weights.clear() - def get_item(self, custom_id: str) -> Item | None: + def get_item(self, custom_id: str) -> Item[V] | None: """Get an item from the view with the given custom ID. Alias for `utils.get(view.children, custom_id=custom_id)`. Parameters @@ -391,7 +393,7 @@ async def on_check_failure(self, interaction: Interaction) -> None: """ async def on_error( - self, error: Exception, item: Item, interaction: Interaction + self, error: Exception, item: Item[V], interaction: Interaction ) -> None: """|coro| @@ -414,7 +416,7 @@ async def on_error( error.__class__, error, error.__traceback__, file=sys.stderr ) - async def _scheduled_task(self, item: Item, interaction: Interaction): + async def _scheduled_task(self, item: Item[V], interaction: Interaction): try: if self.timeout: self.__timeout_expiry = time.monotonic() + self.timeout @@ -446,7 +448,7 @@ def _dispatch_timeout(self): self.on_timeout(), name=f"discord-ui-view-timeout-{self.id}" ) - def _dispatch_item(self, item: Item, interaction: Interaction): + def _dispatch_item(self, item: Item[V], interaction: Interaction): if self.__stopped.done(): return @@ -460,10 +462,10 @@ def _dispatch_item(self, item: Item, interaction: Interaction): def refresh(self, components: list[Component]): # This is pretty hacky at the moment - old_state: dict[tuple[int, str], Item] = { + old_state: dict[tuple[int, str], Item[V]] = { (item.type.value, item.custom_id): item for item in self.children if item.is_dispatchable() # type: ignore } - children: list[Item] = [ + children: list[Item[V]] = [ item for item in self.children if not item.is_dispatchable() ] for component in _walk_all_components(components): @@ -529,7 +531,7 @@ async def wait(self) -> bool: """ return await self.__stopped - def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + def disable_all_items(self, *, exclusions: list[Item[V]] | None = None) -> None: """ Disables all items in the view. @@ -542,7 +544,7 @@ def disable_all_items(self, *, exclusions: list[Item] | None = None) -> None: if exclusions is None or child not in exclusions: child.disabled = True - def enable_all_items(self, *, exclusions: list[Item] | None = None) -> None: + def enable_all_items(self, *, exclusions: list[Item[V]] | None = None) -> None: """ Enables all items in the view. @@ -567,7 +569,7 @@ def message(self, value): class ViewStore: def __init__(self, state: ConnectionState): # (component_type, message_id, custom_id): (View, Item) - self._views: dict[tuple[int, int | None, str], tuple[View, Item]] = {} + self._views: dict[tuple[int, int | None, str], tuple[View, Item[V]]] = {} # message_id: View self._synced_message_views: dict[int, View] = {} self._state: ConnectionState = state diff --git a/discord/utils.py b/discord/utils.py index 363d339391..b509162cf0 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -39,7 +39,6 @@ import warnings from base64 import b64encode from bisect import bisect_left -from dataclasses import field from inspect import isawaitable as _isawaitable from inspect import signature as _signature from operator import attrgetter @@ -115,11 +114,6 @@ def __repr__(self) -> str: MISSING: Any = _MissingSentinel() -# As of 3.11, directly setting a dataclass field to MISSING causes a ValueError. Using -# field(default=MISSING) produces the same error, but passing a lambda to -# default_factory produces the same behavior as default=MISSING and does not raise an -# error. -MissingField = field(default_factory=lambda: MISSING) class _cached_property: @@ -939,7 +933,7 @@ def replacement(match): regex = _MARKDOWN_STOCK_REGEX if ignore_links: regex = f"(?:{_URL_REGEX}|{regex})" - return re.sub(regex, replacement, text, 0, re.MULTILINE) + return re.sub(regex, replacement, text, count=0, flags=re.MULTILINE) def escape_markdown( @@ -981,7 +975,7 @@ def replacement(match): regex = _MARKDOWN_STOCK_REGEX if ignore_links: regex = f"(?:{_URL_REGEX}|{regex})" - return re.sub(regex, replacement, text, 0, re.MULTILINE | re.X) + return re.sub(regex, replacement, text, count=0, flags=re.MULTILINE | re.X) else: text = re.sub(r"\\", r"\\\\", text) return _MARKDOWN_ESCAPE_REGEX.sub(r"\\\1", text) diff --git a/discord/webhook/async_.py b/discord/webhook/async_.py index 1661b1bb67..3edc81d8e2 100644 --- a/discord/webhook/async_.py +++ b/discord/webhook/async_.py @@ -337,16 +337,12 @@ def execute_webhook( multipart: list[dict[str, Any]] | None = None, files: list[File] | None = None, thread_id: int | None = None, - thread_name: str | None = None, wait: bool = False, ) -> Response[MessagePayload | None]: params = {"wait": int(wait)} if thread_id: params["thread_id"] = thread_id - if thread_name: - payload["thread_name"] = thread_name - route = Route( "POST", "/webhooks/{webhook_id}/{webhook_token}", @@ -633,6 +629,7 @@ def handle_message_parameters( allowed_mentions: AllowedMentions | None = MISSING, previous_allowed_mentions: AllowedMentions | None = None, suppress: bool = False, + thread_name: str | None = None, ) -> ExecuteWebhookParameters: if files is not MISSING and file is not MISSING: raise TypeError("Cannot mix file and files keyword arguments.") @@ -717,6 +714,9 @@ def handle_message_parameters( payload["flags"] = flags.value + if thread_name: + payload["thread_name"] = thread_name + if multipart_files: multipart.append({"name": "payload_json", "value": utils._to_json(payload)}) payload = None @@ -1808,6 +1808,7 @@ async def send( applied_tags=applied_tags, allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, + thread_name=thread_name, ) adapter = async_context.get() thread_id: int | None = None @@ -1824,7 +1825,6 @@ async def send( multipart=params.multipart, files=params.files, thread_id=thread_id, - thread_name=thread_name, wait=wait, ) diff --git a/discord/webhook/sync.py b/discord/webhook/sync.py index d2d3213d71..fde7031321 100644 --- a/discord/webhook/sync.py +++ b/discord/webhook/sync.py @@ -307,16 +307,12 @@ def execute_webhook( multipart: list[dict[str, Any]] | None = None, files: list[File] | None = None, thread_id: int | None = None, - thread_name: str | None = None, wait: bool = False, ): params = {"wait": int(wait)} if thread_id: params["thread_id"] = thread_id - if thread_name: - payload["thread_name"] = thread_name - route = Route( "POST", "/webhooks/{webhook_id}/{webhook_token}", @@ -1080,6 +1076,7 @@ def send( allowed_mentions=allowed_mentions, previous_allowed_mentions=previous_mentions, suppress=suppress, + thread_name=thread_name, ) adapter: WebhookAdapter = _get_webhook_adapter() thread_id: int | None = None @@ -1094,7 +1091,6 @@ def send( multipart=params.multipart, files=params.files, thread_id=thread_id, - thread_name=thread_name, wait=wait, ) if wait: diff --git a/pyproject.toml b/pyproject.toml index cc04018145..b75c5e5bf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ - "setuptools>=62.6,<=78.1.0", - "setuptools-scm>=6.2,<=8.2.1", + "setuptools>=62.6,<=80.9.0", + "setuptools-scm>=6.2,<=8.3.1", ] build-backend = "setuptools.build_meta" @@ -13,10 +13,11 @@ authors = [ description = "A Python wrapper for the Discord API" readme = {content-type = "text/x-rst", file = "README.rst"} requires-python = ">=3.9" -license = {text = "MIT"} +license = "MIT" +license-files = ["LICENSE"] classifiers = [ "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: MIT License", + "License-Expression :: MIT", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", diff --git a/requirements/dev.txt b/requirements/dev.txt index 95b2ba1a9a..9f7f1b17d0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,5 @@ -r _.txt -pylint~=3.3.6 +pylint~=3.3.7 pytest~=8.3.5 pytest-asyncio~=0.24.0 # pytest-order~=1.0.1