diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000000..ccbf0ff248
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,3 @@
+# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
+/.github @DisnakeDev/maintainers
+/scripts/ci @DisnakeDev/maintainers
diff --git a/.github/workflows/build-release.yaml b/.github/workflows/build-release.yaml
new file mode 100644
index 0000000000..692affbd8b
--- /dev/null
+++ b/.github/workflows/build-release.yaml
@@ -0,0 +1,227 @@
+# SPDX-License-Identifier: MIT
+
+name: Build (+ Release)
+
+# test build for commit/tag, but only upload release for tags
+on:
+ push:
+ branches:
+ - "master"
+ - 'v[0-9]+.[0-9]+.x' # matches to backport branches, e.g. v3.6.x
+ tags:
+ - "v[0-9]+.[0-9]+.[0-9]+"
+
+permissions:
+ contents: read
+
+jobs:
+ # Builds sdist and wheel, runs `twine check`, and optionally uploads artifacts.
+ build:
+ name: Build package
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up environment
+ id: setup
+ uses: ./.github/actions/setup-env
+ with:
+ python-version: 3.8
+
+ - name: Install dependencies
+ run: pdm install -dG build
+
+ - name: Build package
+ run: |
+ pdm run python -m build
+ ls -la dist/
+
+ - name: Twine check
+ run: pdm run twine check --strict dist/*
+
+ - name: Show metadata
+ run: |
+ mkdir out/
+ tar -xf dist/*.tar.gz -C out/
+
+ echo -e "Metadata
\n" >> $GITHUB_STEP_SUMMARY
+ cat out/*/PKG-INFO | sed 's/^/ /' | tee -a $GITHUB_STEP_SUMMARY
+ echo -e "\n \n" >> $GITHUB_STEP_SUMMARY
+
+ - name: Upload artifact
+ # only upload artifacts when necessary
+ if: startsWith(github.ref, 'refs/tags/')
+ uses: actions/upload-artifact@v3
+ with:
+ name: dist
+ path: dist/
+ if-no-files-found: error
+
+
+ ### Anything below this only runs for tags ###
+
+ # Ensures that git tag and built version match.
+ validate-tag:
+ name: Validate tag
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ needs:
+ - build
+ env:
+ GIT_TAG: ${{ github.ref_name }}
+ outputs:
+ bump_dev: ${{ steps.check-dev.outputs.bump_dev }}
+
+ steps:
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: dist
+ path: dist/
+
+ - name: Compare sdist version to git tag
+ run: |
+ mkdir out/
+ tar -xf dist/*.tar.gz -C out/
+
+ SDIST_VERSION="$(grep "^Version:" out/*/PKG-INFO | cut -d' ' -f2-)"
+ echo "git tag: $GIT_TAG"
+ echo "sdist version: $SDIST_VERSION"
+
+ if [ "$GIT_TAG" != "v$SDIST_VERSION" ]; then
+ echo "error: git tag does not match sdist version" >&2
+ exit 1
+ fi
+
+ - name: Determine if dev version PR is needed
+ id: check-dev
+ run: |
+ BUMP_DEV=
+ # if this is a new major/minor version, create a PR later
+ if [[ "$GIT_TAG" =~ ^v[0-9]+\.[0-9]+\.0$ ]]; then
+ BUMP_DEV=1
+ fi
+ echo "bump_dev=$BUMP_DEV" | tee -a $GITHUB_OUTPUT
+
+
+ # Creates a draft release on GitHub, and uploads the artifacts there.
+ release-github:
+ name: Create GitHub draft release
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ needs:
+ - build
+ - validate-tag
+ permissions:
+ contents: write # required for creating releases
+
+ steps:
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: dist
+ path: dist/
+
+ - name: Calculate versions
+ id: versions
+ env:
+ GIT_TAG: ${{ github.ref_name }}
+ run: |
+ # v1.2.3 -> v1-2-3 (for changelog)
+ echo "docs_version=${GIT_TAG//./-}" >> $GITHUB_OUTPUT
+
+ - name: Create Release
+ uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15
+ with:
+ files: dist/*
+ draft: true
+ body: |
+ TBD.
+
+ **Changelog**: https://docs.disnake.dev/en/stable/whats_new.html#${{ steps.versions.outputs.docs_version }}
+ **Git history**: https://github.com/${{ github.repository }}/compare/vTODO...${{ github.ref_name }}
+
+
+ # Creates a PyPI release (using an environment which requires separate confirmation).
+ release-pypi:
+ name: Publish package to pypi.org
+ environment:
+ name: release-pypi
+ url: https://pypi.org/project/disnake/
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+ needs:
+ - build
+ - validate-tag
+ permissions:
+ id-token: write # this permission is mandatory for trusted publishing
+
+ steps:
+ - name: Download build artifact
+ uses: actions/download-artifact@v3
+ with:
+ name: dist
+ path: dist/
+
+ - name: Upload to pypi
+ uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # v1.8.7
+ with:
+ print-hash: true
+
+
+ # Creates a PR to bump to an alpha version for development, if applicable.
+ create-dev-version-pr:
+ name: Create dev version bump PR
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/') && needs.validate-tag.outputs.bump_dev
+ needs:
+ - validate-tag
+ - release-github
+ - release-pypi
+
+ steps:
+ # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow
+ - name: Generate app token
+ id: generate_token
+ uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
+ with:
+ app_id: ${{ secrets.BOT_APP_ID }}
+ private_key: ${{ secrets.BOT_PRIVATE_KEY }}
+
+ - uses: actions/checkout@v3
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ persist-credentials: false
+ ref: master # the PR action wants a proper base branch
+
+ - name: Set git name/email
+ env:
+ GIT_USER: ${{ vars.GIT_APP_USER_NAME }}
+ GIT_EMAIL: ${{ vars.GIT_APP_USER_EMAIL }}
+ run: |
+ git config user.name "$GIT_USER"
+ git config user.email "$GIT_EMAIL"
+
+ - name: Update version to dev
+ id: update-version
+ run: |
+ NEW_VERSION="$(python scripts/ci/versiontool.py --set dev)"
+ git commit -a -m "chore: update version to v$NEW_VERSION"
+ echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
+
+ - name: Create pull request
+ uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ branch: auto/dev-v${{ steps.update-version.outputs.new_version }}
+ delete-branch: true
+ base: master
+ title: "chore: update version to v${{ steps.update-version.outputs.new_version }}"
+ body: |
+ Automated dev version PR.
+
+ https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ labels: |
+ skip news
+ t: meta
diff --git a/.github/workflows/changelog.yaml b/.github/workflows/changelog.yaml
index 8fdb8e27c7..f2a989c0c4 100644
--- a/.github/workflows/changelog.yaml
+++ b/.github/workflows/changelog.yaml
@@ -33,7 +33,7 @@ jobs:
python-version: '3.9'
- name: Install dependencies
- run: pdm install -dG tools
+ run: pdm install -dG changelog
- name: Check for presence of a Change Log fragment (only pull requests)
# NOTE: The pull request' base branch needs to be fetched so towncrier
diff --git a/.github/workflows/create-release-pr.yaml b/.github/workflows/create-release-pr.yaml
new file mode 100644
index 0000000000..56c32a4ac5
--- /dev/null
+++ b/.github/workflows/create-release-pr.yaml
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: MIT
+
+name: Create Release PR
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "The new version number, e.g. `1.2.3`."
+ type: string
+ required: true
+
+permissions: {}
+
+jobs:
+ create-release-pr:
+ name: Create Release PR
+ runs-on: ubuntu-latest
+
+ env:
+ VERSION_INPUT: ${{ inputs.version }}
+
+ steps:
+ # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow
+ - name: Generate app token
+ id: generate_token
+ uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
+ with:
+ app_id: ${{ secrets.BOT_APP_ID }}
+ private_key: ${{ secrets.BOT_PRIVATE_KEY }}
+
+ - uses: actions/checkout@v3
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ persist-credentials: false
+
+ - name: Set git name/email
+ env:
+ GIT_USER: ${{ vars.GIT_APP_USER_NAME }}
+ GIT_EMAIL: ${{ vars.GIT_APP_USER_EMAIL }}
+ run: |
+ git config user.name "$GIT_USER"
+ git config user.email "$GIT_EMAIL"
+
+ - name: Set up environment
+ uses: ./.github/actions/setup-env
+ with:
+ python-version: 3.8
+
+ - name: Install dependencies
+ run: pdm install -dG changelog
+
+ - name: Update version
+ run: |
+ python scripts/ci/versiontool.py --set "$VERSION_INPUT"
+ git commit -a -m "chore: update version to $VERSION_INPUT"
+
+ - name: Build changelog
+ run: |
+ pdm run towncrier build --yes --version "$VERSION_INPUT"
+ git commit -a -m "docs: build changelog"
+
+ - name: Create pull request
+ uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ branch: auto/release-v${{ inputs.version }}
+ delete-branch: true
+ title: "release: v${{ inputs.version }}"
+ body: |
+ Automated release PR, triggered by @${{ github.actor }} for ${{ github.sha }}.
+
+ ### Tasks
+ - [ ] Add changelogs from backports, if applicable.
+ - [ ] Once merged, create + push a tag.
+
+ https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ labels: |
+ t: release
+ assignees: |
+ ${{ github.actor }}
diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml
index e49635f821..64dc2e18a7 100644
--- a/.github/workflows/lint-test.yml
+++ b/.github/workflows/lint-test.yml
@@ -6,7 +6,7 @@ on:
push:
branches:
- 'master'
- - 'v[0-9]+.[0-9]+.x' # matches to backport branches, e.g. 3.6
+ - 'v[0-9]+.[0-9]+.x' # matches to backport branches, e.g. v3.6.x
- 'run-ci/*'
tags:
pull_request:
@@ -54,7 +54,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
experimental: [false]
fail-fast: false
continue-on-error: ${{ matrix.experimental }}
@@ -72,7 +72,9 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
- run: pdm install -d -Gspeed -Gdocs -Gvoice
+ run: |
+ pdm update --pre aiohttp # XXX: temporarily install aiohttp prerelease for 3.12
+ pdm install -d -Gspeed -Gdocs -Gvoice
- name: Add .venv/bin to PATH
run: dirname "$(pdm info --python)" >> $GITHUB_PATH
@@ -122,15 +124,14 @@ jobs:
run: nox -s check-manifest
# This only runs if the previous steps were successful, no point in running it otherwise
- - name: Build package
+ - name: Try building package
run: |
- python -m pip install -U build
- python -m build
+ pdm install -dG build
+ pdm run python -m build
+ ls -la dist/
# run the libcst parsers and check for changes
- name: libcst codemod
- env:
- LIBCST_PARSER_TYPE: "native"
run: |
nox -s codemod -- run-all
if [ -n "$(git status --porcelain)" ]; then
@@ -147,7 +148,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- python-version: ["3.8", "3.9", "3.10", "3.11"]
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
os: ["windows-latest", "ubuntu-latest", "macos-latest"]
experimental: [false]
fail-fast: true
@@ -167,11 +168,13 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
- run: pdm install -dG test # needed for coverage
+ run: |
+ pdm update --pre aiohttp # XXX: temporarily install aiohttp prerelease for 3.12
+ pdm install -dG test # needed for coverage
- name: Test package install
run: |
- python -m pip install .
+ python -m pip install --pre . # XXX: temporarily install aiohttp prerelease for 3.12; remove --pre flag again later
- name: Run pytest
id: run_tests
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index db6ec422f0..104e2264a4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -35,7 +35,7 @@ repos:
args: [--negate]
types: [text]
exclude_types: [json, pofile]
- exclude: 'changelog/|py.typed|disnake/bin/COPYING|.github/PULL_REQUEST_TEMPLATE.md|LICENSE|MANIFEST.in'
+ exclude: 'changelog/|py.typed|disnake/bin/COPYING|.github/PULL_REQUEST_TEMPLATE.md|.github/CODEOWNERS|LICENSE|MANIFEST.in'
- repo: https://github.com/pycqa/isort
rev: 5.12.0
@@ -56,6 +56,6 @@ repos:
name: "run black in all files"
- repo: https://github.com/charliermarsh/ruff-pre-commit
- rev: v0.0.261
+ rev: v0.0.292
hooks:
- id: ruff
diff --git a/RELEASE.md b/RELEASE.md
new file mode 100644
index 0000000000..3919b696e5
--- /dev/null
+++ b/RELEASE.md
@@ -0,0 +1,47 @@
+
+
+# Release Procedure
+
+This document provides general information and steps about the project's release procedure.
+If you're reading this, this will likely not be useful to you, unless you have administrator permissions in the repository or want to replicate this setup in your own project :p
+
+The process is largely automated, with manual action only being needed where higher permissions are required.
+Note that pre-releases (alpha/beta/rc) don't quite work with the current setup; we don't currently anticipate making pre-releases, but this may still be improved in the future.
+
+
+## Steps
+
+These steps are mostly equivalent for major/minor (feature) and micro (bugfix) releases.
+The branch should be `master` for major/minor releases and e.g. `1.2.x` for micro releases.
+
+1. Run the `Create Release PR` workflow from the GitHub UI (or CLI), specifying the correct branch and new version.
+ 1. Wait until a PR containing the changelog and version bump is created. Update the changelog description and merge the PR.
+ 2. In the CLI, fetch changes and create + push a tag for the newly created commit, which will trigger another workflow.
+ - [if latest] Also force-push a `stable` tag for the same ref.
+ 3. Update the visibility of old/new versions on https://readthedocs.org.
+2. Approve the environment deployment when prompted, which will push the package to PyPI.
+ 1. Update and publish the created GitHub draft release, as well as a Discord announcement. 🎉
+3. [if major/minor] Create a `v1.2.x` branch for future backports, and merge the newly created dev version PR.
+
+
+### Manual Steps
+
+If the automated process above does not work for some reason, here's the abridged version of the manual release process:
+
+1. Update version in `__init__.py`, run `towncrier build`. Commit, push, create + merge PR.
+2. Follow steps 1.ii. + 1.iii. like above.
+3. Run `python -m build`, attach artifacts to GitHub release.
+4. Run `twine check dist/*` + `twine upload dist/*`.
+5. Follow steps 2.i. + 3. like above.
+
+
+## Repository Setup
+
+This automated process requires some initial one-time setup in the repository to work properly:
+
+1. Create a GitHub App ([docs](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow)), enable write permissions for `content` and `pull_requests`.
+2. Install the app in the repository.
+3. Set repository variables `GIT_APP_USER_NAME` and `GIT_APP_USER_EMAIL` accordingly.
+4. Set repository secrets `BOT_APP_ID` and `BOT_PRIVATE_KEY`.
+5. Create a `release-pypi` environment, add protection rules.
+6. Set up trusted publishing on PyPI ([docs](https://docs.pypi.org/trusted-publishers/adding-a-publisher/)).
diff --git a/changelog/1045.bugfix.rst b/changelog/1045.bugfix.rst
new file mode 100644
index 0000000000..baa41c937a
--- /dev/null
+++ b/changelog/1045.bugfix.rst
@@ -0,0 +1 @@
+|commands| Fix incorrect typings of :meth:`~disnake.ext.commands.InvokableApplicationCommand.add_check`, :meth:`~disnake.ext.commands.InvokableApplicationCommand.remove_check`, :meth:`~disnake.ext.commands.InteractionBotBase.add_app_command_check` and :meth:`~disnake.ext.commands.InteractionBotBase.remove_app_command_check`.
diff --git a/changelog/1045.feature.rst b/changelog/1045.feature.rst
new file mode 100644
index 0000000000..85af28ec63
--- /dev/null
+++ b/changelog/1045.feature.rst
@@ -0,0 +1 @@
+|commands| Implement :func:`~disnake.ext.commands.app_check` and :func:`~disnake.ext.commands.app_check_any` decorators.
diff --git a/changelog/1091.feature.rst b/changelog/1091.feature.rst
new file mode 100644
index 0000000000..6ae43e7e5f
--- /dev/null
+++ b/changelog/1091.feature.rst
@@ -0,0 +1 @@
+Add :attr:`Permissions.create_guild_expressions` and :attr:`Permissions.create_events`.
diff --git a/changelog/1094.doc.rst b/changelog/1094.doc.rst
new file mode 100644
index 0000000000..13fe750a4b
--- /dev/null
+++ b/changelog/1094.doc.rst
@@ -0,0 +1 @@
+Add inherited attributes to :class:`TeamMember`, and fix :attr:`TeamMember.avatar` documentation.
diff --git a/changelog/1094.feature.0.rst b/changelog/1094.feature.0.rst
new file mode 100644
index 0000000000..dcb2dcf367
--- /dev/null
+++ b/changelog/1094.feature.0.rst
@@ -0,0 +1 @@
+Add :attr:`TeamMember.role`.
diff --git a/changelog/1094.feature.1.rst b/changelog/1094.feature.1.rst
new file mode 100644
index 0000000000..1b7eb2cd33
--- /dev/null
+++ b/changelog/1094.feature.1.rst
@@ -0,0 +1 @@
+|commands| Update :meth:`Bot.is_owner ` to take team member roles into account.
diff --git a/changelog/1111.bugfix.rst b/changelog/1111.bugfix.rst
new file mode 100644
index 0000000000..4efa8693ed
--- /dev/null
+++ b/changelog/1111.bugfix.rst
@@ -0,0 +1 @@
+Allow ``cls`` argument in select menu decorators (e.g. :func`ui.string_select`) to be specified by keyword instead of being positional-only.
diff --git a/changelog/1112.doc.rst b/changelog/1112.doc.rst
new file mode 100644
index 0000000000..e510f78951
--- /dev/null
+++ b/changelog/1112.doc.rst
@@ -0,0 +1 @@
+Document the :class:`.Option` attributes, the ``description`` and ``options`` properties for :class:`.ext.commands.InvokableSlashCommand` and the ``description`` and ``body`` properties for :class:`.ext.commands.SubCommand`.
diff --git a/changelog/1116.misc.rst b/changelog/1116.misc.rst
new file mode 100644
index 0000000000..7e17a486ef
--- /dev/null
+++ b/changelog/1116.misc.rst
@@ -0,0 +1 @@
+|commands| Rewrite slash command signature evaluation to use the same mechanism as prefix command signatures. This should not have an impact on user code, but streamlines future changes.
diff --git a/changelog/1117.misc.rst b/changelog/1117.misc.rst
new file mode 100644
index 0000000000..3a10d0f07e
--- /dev/null
+++ b/changelog/1117.misc.rst
@@ -0,0 +1 @@
+Start testing with Python 3.12 in CI.
diff --git a/changelog/1120.bugfix.rst b/changelog/1120.bugfix.rst
new file mode 100644
index 0000000000..146ac4a8de
--- /dev/null
+++ b/changelog/1120.bugfix.rst
@@ -0,0 +1 @@
+|commands| Fix edge case in evaluation of multiple identical annotations with forwardrefs in a single signature.
diff --git a/changelog/1121.feature.rst b/changelog/1121.feature.rst
new file mode 100644
index 0000000000..1294ba4044
--- /dev/null
+++ b/changelog/1121.feature.rst
@@ -0,0 +1 @@
+Make :class:`Interaction` and subtypes accept the bot type as a generic parameter to denote the type returned by the :attr:`~Interaction.bot` and :attr:`~Interaction.client` properties.
diff --git a/changelog/1123.bugfix.rst b/changelog/1123.bugfix.rst
new file mode 100644
index 0000000000..625f275381
--- /dev/null
+++ b/changelog/1123.bugfix.rst
@@ -0,0 +1 @@
+Fix :meth:`Thread.permissions_for` not working in some cases due to an incorrect import.
diff --git a/changelog/1126.doc.rst b/changelog/1126.doc.rst
new file mode 100644
index 0000000000..44fa13bf31
--- /dev/null
+++ b/changelog/1126.doc.rst
@@ -0,0 +1 @@
+Make all "Supported Operations" container elements collapsible.
diff --git a/changelog/1128.feature.rst b/changelog/1128.feature.rst
new file mode 100644
index 0000000000..66c35b1935
--- /dev/null
+++ b/changelog/1128.feature.rst
@@ -0,0 +1 @@
+|commands| Support Python 3.12's ``type`` statement and :class:`py:typing.TypeAliasType` annotations in command signatures.
diff --git a/changelog/1133.bugfix.rst b/changelog/1133.bugfix.rst
new file mode 100644
index 0000000000..fdf45729eb
--- /dev/null
+++ b/changelog/1133.bugfix.rst
@@ -0,0 +1 @@
+|commands| Fix erroneous :class:`LocalizationWarning`\s when using localized slash command parameters in cogs.
diff --git a/changelog/1134.misc.rst b/changelog/1134.misc.rst
new file mode 100644
index 0000000000..664f4a332d
--- /dev/null
+++ b/changelog/1134.misc.rst
@@ -0,0 +1 @@
+Add :class:`StandardSticker` to ``stickers`` parameter type annotation of :meth:`Messageable.send` and :meth:`ForumChannel.create_thread`.
diff --git a/changelog/1136.bugfix.rst b/changelog/1136.bugfix.rst
new file mode 100644
index 0000000000..571cba6cbd
--- /dev/null
+++ b/changelog/1136.bugfix.rst
@@ -0,0 +1 @@
+Update ``choices`` type in app commands to accept any :class:`~py:typing.Sequence` or :class:`~py:typing.Mapping`, instead of the more constrained :class:`list`/:class:`dict` types.
diff --git a/changelog/847.feature.rst b/changelog/847.feature.rst
new file mode 100644
index 0000000000..7418ed0783
--- /dev/null
+++ b/changelog/847.feature.rst
@@ -0,0 +1 @@
+|commands| Skip evaluating annotations of ``self`` (if present) and ``ctx`` parameters in prefix commands. These may now use stringified annotations with types that aren't available at runtime.
diff --git a/disnake/__init__.py b/disnake/__init__.py
index 396bab5e43..0cfd1b53d0 100644
--- a/disnake/__init__.py
+++ b/disnake/__init__.py
@@ -81,6 +81,8 @@ class VersionInfo(NamedTuple):
serial: int
+# fmt: off
version_info: VersionInfo = VersionInfo(major=2, minor=10, micro=0, releaselevel="alpha", serial=0)
+# fmt: on
logging.getLogger(__name__).addHandler(logging.NullHandler())
diff --git a/disnake/abc.py b/disnake/abc.py
index 605bb725aa..fda31ec5fb 100644
--- a/disnake/abc.py
+++ b/disnake/abc.py
@@ -42,7 +42,7 @@
from .partial_emoji import PartialEmoji
from .permissions import PermissionOverwrite, Permissions
from .role import Role
-from .sticker import GuildSticker, StickerItem
+from .sticker import GuildSticker, StandardSticker, StickerItem
from .ui.action_row import components_to_dict
from .utils import _overload_with_permissions
from .voice_client import VoiceClient, VoiceProtocol
@@ -390,7 +390,7 @@ async def _edit(
if p_id is not None and (parent := self.guild.get_channel(p_id)):
overwrites_payload = [c._asdict() for c in parent._overwrites]
- if overwrites is not MISSING and overwrites is not None:
+ if overwrites not in (MISSING, None):
overwrites_payload = []
for target, perm in overwrites.items():
if not isinstance(perm, PermissionOverwrite):
@@ -853,7 +853,9 @@ async def set_permissions(
ban_members: Optional[bool] = ...,
change_nickname: Optional[bool] = ...,
connect: Optional[bool] = ...,
+ create_events: Optional[bool] = ...,
create_forum_threads: Optional[bool] = ...,
+ create_guild_expressions: Optional[bool] = ...,
create_instant_invite: Optional[bool] = ...,
create_private_threads: Optional[bool] = ...,
create_public_threads: Optional[bool] = ...,
@@ -1427,7 +1429,7 @@ async def send(
tts: bool = ...,
embed: Embed = ...,
file: File = ...,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
suppress_embeds: bool = ...,
@@ -1448,7 +1450,7 @@ async def send(
tts: bool = ...,
embed: Embed = ...,
files: List[File] = ...,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
suppress_embeds: bool = ...,
@@ -1469,7 +1471,7 @@ async def send(
tts: bool = ...,
embeds: List[Embed] = ...,
file: File = ...,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
suppress_embeds: bool = ...,
@@ -1490,7 +1492,7 @@ async def send(
tts: bool = ...,
embeds: List[Embed] = ...,
files: List[File] = ...,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ...,
delete_after: float = ...,
nonce: Union[str, int] = ...,
suppress_embeds: bool = ...,
@@ -1512,7 +1514,7 @@ async def send(
embeds: Optional[List[Embed]] = None,
file: Optional[File] = None,
files: Optional[List[File]] = None,
- stickers: Optional[Sequence[Union[GuildSticker, StickerItem]]] = None,
+ stickers: Optional[Sequence[Union[GuildSticker, StandardSticker, StickerItem]]] = None,
delete_after: Optional[float] = None,
nonce: Optional[Union[str, int]] = None,
suppress_embeds: Optional[bool] = None,
@@ -1565,7 +1567,7 @@ async def send(
files: List[:class:`.File`]
A list of files to upload. Must be a maximum of 10.
This cannot be mixed with the ``file`` parameter.
- stickers: Sequence[Union[:class:`.GuildSticker`, :class:`.StickerItem`]]
+ stickers: Sequence[Union[:class:`.GuildSticker`, :class:`.StandardSticker`, :class:`.StickerItem`]]
A list of stickers to upload. Must be a maximum of 3.
.. versionadded:: 2.0
diff --git a/disnake/activity.py b/disnake/activity.py
index a213bf5a75..3c290edd17 100644
--- a/disnake/activity.py
+++ b/disnake/activity.py
@@ -404,7 +404,7 @@ class Game(BaseActivity):
This is typically displayed via **Playing** on the official Discord client.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -487,7 +487,7 @@ class Streaming(BaseActivity):
This is typically displayed via **Streaming** on the official Discord client.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -597,7 +597,7 @@ def __hash__(self) -> int:
class Spotify(_BaseActivity):
"""Represents a Spotify listening activity from Discord.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -770,7 +770,7 @@ def party_id(self) -> str:
class CustomActivity(BaseActivity):
"""Represents a Custom activity from Discord.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -921,7 +921,7 @@ def create_activity(
elif game_type is ActivityType.listening and "sync_id" in data and "session_id" in data:
activity = Spotify(**data)
else:
- activity = Activity(**data)
+ activity = Activity(**data) # type: ignore
if isinstance(activity, (Activity, CustomActivity)) and activity.emoji and state:
activity.emoji._state = state
diff --git a/disnake/app_commands.py b/disnake/app_commands.py
index 17c3fd713a..727f35cb93 100644
--- a/disnake/app_commands.py
+++ b/disnake/app_commands.py
@@ -5,7 +5,7 @@
import math
import re
from abc import ABC
-from typing import TYPE_CHECKING, ClassVar, Dict, List, Mapping, Optional, Tuple, Union
+from typing import TYPE_CHECKING, ClassVar, List, Mapping, Optional, Sequence, Tuple, Union
from .enums import (
ApplicationCommandPermissionType,
@@ -37,10 +37,10 @@
)
Choices = Union[
- List["OptionChoice"],
- List[ApplicationCommandOptionChoiceValue],
- Dict[str, ApplicationCommandOptionChoiceValue],
- List[Localized[str]],
+ Sequence["OptionChoice"],
+ Sequence[ApplicationCommandOptionChoiceValue],
+ Mapping[str, ApplicationCommandOptionChoiceValue],
+ Sequence[Localized[str]],
]
APIApplicationCommand = Union["APIUserCommand", "APIMessageCommand", "APISlashCommand"]
@@ -179,8 +179,42 @@ class Option:
The option type, e.g. :class:`OptionType.user`.
required: :class:`bool`
Whether this option is required.
- choices: Union[List[:class:`OptionChoice`], List[Union[:class:`str`, :class:`int`]], Dict[:class:`str`, Union[:class:`str`, :class:`int`]]]
- The list of option choices.
+ choices: Union[Sequence[:class:`OptionChoice`], Sequence[Union[:class:`str`, :class:`int`, :class:`float`]], Mapping[:class:`str`, Union[:class:`str`, :class:`int`, :class:`float`]]]
+ The pre-defined choices for this option.
+ options: List[:class:`Option`]
+ The list of sub options. Normally you don't have to specify it directly,
+ instead consider using ``@main_cmd.sub_command`` or ``@main_cmd.sub_command_group`` decorators.
+ channel_types: List[:class:`ChannelType`]
+ The list of channel types that your option supports, if the type is :class:`OptionType.channel`.
+ By default, it supports all channel types.
+ autocomplete: :class:`bool`
+ Whether this option can be autocompleted.
+ min_value: Union[:class:`int`, :class:`float`]
+ The minimum value permitted.
+ max_value: Union[:class:`int`, :class:`float`]
+ The maximum value permitted.
+ min_length: :class:`int`
+ The minimum length for this option if this is a string option.
+
+ .. versionadded:: 2.6
+
+ max_length: :class:`int`
+ The maximum length for this option if this is a string option.
+
+ .. versionadded:: 2.6
+
+ Attributes
+ ----------
+ name: :class:`str`
+ The option's name.
+ description: :class:`str`
+ The option's description.
+ type: :class:`OptionType`
+ The option type, e.g. :class:`OptionType.user`.
+ required: :class:`bool`
+ Whether this option is required.
+ choices: List[:class:`OptionChoice`]
+ The list of pre-defined choices.
options: List[:class:`Option`]
The list of sub options. Normally you don't have to specify it directly,
instead consider using ``@main_cmd.sub_command`` or ``@main_cmd.sub_command_group`` decorators.
@@ -270,6 +304,9 @@ def __init__(
if autocomplete:
raise TypeError("can not specify both choices and autocomplete args")
+ if isinstance(choices, str): # str matches `Sequence[str]`, but isn't meant to be used
+ raise TypeError("choices argument should be a list/sequence or dict, not str")
+
if isinstance(choices, Mapping):
self.choices = [OptionChoice(name, value) for name, value in choices.items()]
else:
@@ -336,7 +373,7 @@ def from_dict(cls, data: ApplicationCommandOptionPayload) -> Option:
def add_choice(
self,
name: LocalizedRequired,
- value: Union[str, int],
+ value: ApplicationCommandOptionChoiceValue,
) -> None:
"""Adds an OptionChoice to the list of current choices,
parameters are the same as for :class:`OptionChoice`.
@@ -354,7 +391,7 @@ def add_option(
description: LocalizedOptional = None,
type: Optional[OptionType] = None,
required: bool = False,
- choices: Optional[List[OptionChoice]] = None,
+ choices: Optional[Choices] = None,
options: Optional[list] = None,
channel_types: Optional[List[ChannelType]] = None,
autocomplete: bool = False,
@@ -850,7 +887,7 @@ def add_option(
description: LocalizedOptional = None,
type: Optional[OptionType] = None,
required: bool = False,
- choices: Optional[List[OptionChoice]] = None,
+ choices: Optional[Choices] = None,
options: Optional[list] = None,
channel_types: Optional[List[ChannelType]] = None,
autocomplete: bool = False,
diff --git a/disnake/asset.py b/disnake/asset.py
index fc8fa6c7ea..bc7b505697 100644
--- a/disnake/asset.py
+++ b/disnake/asset.py
@@ -24,7 +24,7 @@
ValidAssetFormatTypes = Literal["webp", "jpeg", "jpg", "png", "gif"]
AnyState = Union[ConnectionState, _WebhookState[BaseWebhook]]
-AssetBytes = Union[bytes, "AssetMixin"]
+AssetBytes = Union[utils._BytesLike, "AssetMixin"]
VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"})
VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"}
@@ -164,7 +164,7 @@ async def to_file(
class Asset(AssetMixin):
"""Represents a CDN asset on Discord.
- .. container:: operations
+ .. collapse:: operations
.. describe:: str(x)
diff --git a/disnake/audit_logs.py b/disnake/audit_logs.py
index 9d45912cd9..256aaa04dc 100644
--- a/disnake/audit_logs.py
+++ b/disnake/audit_logs.py
@@ -245,7 +245,7 @@ def _transform_datetime(entry: AuditLogEntry, data: Optional[str]) -> Optional[d
def _transform_privacy_level(
- entry: AuditLogEntry, data: int
+ entry: AuditLogEntry, data: Optional[int]
) -> Optional[Union[enums.StagePrivacyLevel, enums.GuildScheduledEventPrivacyLevel]]:
if data is None:
return None
@@ -517,7 +517,7 @@ class AuditLogEntry(Hashable):
You can retrieve these via :meth:`Guild.audit_logs`,
or via the :func:`on_audit_log_entry_create` event.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/channel.py b/disnake/channel.py
index 263735e24e..042611b563 100644
--- a/disnake/channel.py
+++ b/disnake/channel.py
@@ -74,7 +74,7 @@
from .message import AllowedMentions, Message, PartialMessage
from .role import Role
from .state import ConnectionState
- from .sticker import GuildSticker, StickerItem
+ from .sticker import GuildSticker, StandardSticker, StickerItem
from .threads import AnyThreadArchiveDuration, ThreadType
from .types.channel import (
CategoryChannel as CategoryChannelPayload,
@@ -103,7 +103,7 @@ async def _single_delete_strategy(messages: Iterable[Message]) -> None:
class TextChannel(disnake.abc.Messageable, disnake.abc.GuildChannel, Hashable):
"""Represents a Discord guild text channel.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -473,7 +473,7 @@ async def edit(
overwrites=overwrites,
flags=flags,
reason=reason,
- **kwargs,
+ **kwargs, # type: ignore
)
if payload is not None:
# the payload will always be the proper channel payload
@@ -1207,6 +1207,7 @@ def permissions_for(
denied.update(
manage_channels=True,
manage_roles=True,
+ create_events=True,
manage_events=True,
manage_webhooks=True,
)
@@ -1217,7 +1218,7 @@ def permissions_for(
class VoiceChannel(disnake.abc.Messageable, VocalGuildChannel):
"""Represents a Discord guild voice channel.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -1628,7 +1629,7 @@ async def edit(
slowmode_delay=slowmode_delay,
flags=flags,
reason=reason,
- **kwargs,
+ **kwargs, # type: ignore
)
if payload is not None:
# the payload will always be the proper channel payload
@@ -1871,7 +1872,7 @@ class StageChannel(disnake.abc.Messageable, VocalGuildChannel):
.. versionadded:: 1.7
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -2453,7 +2454,7 @@ async def edit(
flags=flags,
slowmode_delay=slowmode_delay,
reason=reason,
- **kwargs,
+ **kwargs, # type: ignore
)
if payload is not None:
# the payload will always be the proper channel payload
@@ -2696,7 +2697,7 @@ class CategoryChannel(disnake.abc.GuildChannel, Hashable):
These are useful to group channels to logical compartments.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -2946,7 +2947,7 @@ async def edit(
overwrites=overwrites,
flags=flags,
reason=reason,
- **kwargs,
+ **kwargs, # type: ignore
)
if payload is not None:
# the payload will always be the proper channel payload
@@ -3145,7 +3146,7 @@ class ForumChannel(disnake.abc.GuildChannel, Hashable):
.. versionadded:: 2.5
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -3619,7 +3620,7 @@ async def edit(
default_sort_order=default_sort_order,
default_layout=default_layout,
reason=reason,
- **kwargs,
+ **kwargs, # type: ignore
)
if payload is not None:
# the payload will always be the proper channel payload
@@ -3790,7 +3791,7 @@ async def create_thread(
file: File = ...,
suppress_embeds: bool = ...,
flags: MessageFlags = ...,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ...,
allowed_mentions: AllowedMentions = ...,
view: View = ...,
components: Components = ...,
@@ -3811,7 +3812,7 @@ async def create_thread(
files: List[File] = ...,
suppress_embeds: bool = ...,
flags: MessageFlags = ...,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ...,
allowed_mentions: AllowedMentions = ...,
view: View = ...,
components: Components = ...,
@@ -3832,7 +3833,7 @@ async def create_thread(
file: File = ...,
suppress_embeds: bool = ...,
flags: MessageFlags = ...,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ...,
allowed_mentions: AllowedMentions = ...,
view: View = ...,
components: Components = ...,
@@ -3853,7 +3854,7 @@ async def create_thread(
files: List[File] = ...,
suppress_embeds: bool = ...,
flags: MessageFlags = ...,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = ...,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = ...,
allowed_mentions: AllowedMentions = ...,
view: View = ...,
components: Components = ...,
@@ -3875,7 +3876,7 @@ async def create_thread(
files: List[File] = MISSING,
suppress_embeds: bool = MISSING,
flags: MessageFlags = MISSING,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = MISSING,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = MISSING,
allowed_mentions: AllowedMentions = MISSING,
view: View = MISSING,
components: Components[MessageUIComponent] = MISSING,
@@ -3939,7 +3940,7 @@ async def create_thread(
files: List[:class:`.File`]
A list of files to upload. Must be a maximum of 10.
This cannot be mixed with the ``file`` parameter.
- stickers: Sequence[Union[:class:`.GuildSticker`, :class:`.StickerItem`]]
+ stickers: Sequence[Union[:class:`.GuildSticker`, :class:`.StandardSticker`, :class:`.StickerItem`]]
A list of stickers to upload. Must be a maximum of 3.
allowed_mentions: :class:`.AllowedMentions`
Controls the mentions being processed in this message. If this is
@@ -3994,7 +3995,7 @@ async def create_thread(
stickers=stickers,
)
- if auto_archive_duration is not None:
+ if auto_archive_duration not in (MISSING, None):
auto_archive_duration = cast(
"ThreadArchiveDurationLiteral", try_enum_to_int(auto_archive_duration)
)
@@ -4184,7 +4185,7 @@ def get_tag_by_name(self, name: str, /) -> Optional[ForumTag]:
class DMChannel(disnake.abc.Messageable, Hashable):
"""Represents a Discord direct message channel.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -4347,7 +4348,7 @@ def get_partial_message(self, message_id: int, /) -> PartialMessage:
class GroupChannel(disnake.abc.Messageable, Hashable):
"""Represents a Discord group channel.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -4506,7 +4507,7 @@ class PartialMessageable(disnake.abc.Messageable, Hashable):
.. versionadded:: 2.0
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/client.py b/disnake/client.py
index f71842c7b3..b25b44cbd9 100644
--- a/disnake/client.py
+++ b/disnake/client.py
@@ -25,6 +25,7 @@
Optional,
Sequence,
Tuple,
+ TypedDict,
TypeVar,
Union,
overload,
@@ -79,6 +80,8 @@
from .widget import Widget
if TYPE_CHECKING:
+ from typing_extensions import NotRequired
+
from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import APIApplicationCommand
from .asset import AssetBytes
@@ -207,6 +210,17 @@ class GatewayParams(NamedTuple):
zlib: bool = True
+# used for typing the ws parameter dict in the connect() loop
+class _WebSocketParams(TypedDict):
+ initial: bool
+ shard_id: Optional[int]
+ gateway: Optional[str]
+
+ sequence: NotRequired[Optional[int]]
+ resume: NotRequired[bool]
+ session: NotRequired[Optional[str]]
+
+
class Client:
"""Represents a client connection that connects to Discord.
This class is used to interact with the Discord WebSocket and API.
@@ -1080,7 +1094,7 @@ async def connect(
if not ignore_session_start_limit and self.session_start_limit.remaining == 0:
raise SessionStartLimitReached(self.session_start_limit)
- ws_params = {
+ ws_params: _WebSocketParams = {
"initial": True,
"shard_id": self.shard_id,
"gateway": initial_gateway,
@@ -1104,6 +1118,7 @@ async def connect(
while True:
await self.ws.poll_event()
+
except ReconnectWebSocket as e:
_log.info("Got a request to %s the websocket.", e.op)
self.dispatch("disconnect")
@@ -1116,6 +1131,7 @@ async def connect(
gateway=self.ws.resume_gateway if e.resume else initial_gateway,
)
continue
+
except (
OSError,
HTTPException,
@@ -1196,7 +1212,8 @@ async def close(self) -> None:
# if an error happens during disconnects, disregard it.
pass
- if self.ws is not None and self.ws.open:
+ # can be None if not connected
+ if self.ws is not None and self.ws.open: # pyright: ignore[reportUnnecessaryComparison]
await self.ws.close(code=1000)
await self.http.close()
@@ -1874,16 +1891,15 @@ async def change_presence(
await self.ws.change_presence(activity=activity, status=status_str)
+ activities = () if activity is None else (activity,)
for guild in self._connection.guilds:
me = guild.me
- if me is None:
+ if me is None: # pyright: ignore[reportUnnecessaryComparison]
+ # may happen if guild is unavailable
continue
- if activity is not None:
- me.activities = (activity,) # type: ignore
- else:
- me.activities = ()
-
+ # Member.activities is typehinted as Tuple[ActivityType, ...], we may be setting it as Tuple[BaseActivity, ...]
+ me.activities = activities # type: ignore
me.status = status
# Guild stuff
diff --git a/disnake/colour.py b/disnake/colour.py
index 82e8ef1bb3..4bd6585ea2 100644
--- a/disnake/colour.py
+++ b/disnake/colour.py
@@ -22,7 +22,7 @@ class Colour:
There is an alias for this called Color.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/components.py b/disnake/components.py
index e6f3d14904..7614fd424b 100644
--- a/disnake/components.py
+++ b/disnake/components.py
@@ -9,6 +9,7 @@
Dict,
Generic,
List,
+ Literal,
Optional,
Tuple,
Type,
@@ -22,11 +23,12 @@
from .utils import MISSING, assert_never, get_slots
if TYPE_CHECKING:
- from typing_extensions import Self
+ from typing_extensions import Self, TypeAlias
from .emoji import Emoji
from .types.components import (
ActionRow as ActionRowPayload,
+ AnySelectMenu as AnySelectMenuPayload,
BaseSelectMenu as BaseSelectMenuPayload,
ButtonComponent as ButtonComponentPayload,
ChannelSelectMenu as ChannelSelectMenuPayload,
@@ -63,12 +65,16 @@
"MentionableSelectMenu",
"ChannelSelectMenu",
]
-MessageComponent = Union["Button", "AnySelectMenu"]
-if TYPE_CHECKING: # TODO: remove when we add modal select support
- from typing_extensions import TypeAlias
+SelectMenuType = Literal[
+ ComponentType.string_select,
+ ComponentType.user_select,
+ ComponentType.role_select,
+ ComponentType.mentionable_select,
+ ComponentType.channel_select,
+]
-# ModalComponent = Union["TextInput", "AnySelectMenu"]
+MessageComponent = Union["Button", "AnySelectMenu"]
ModalComponent: TypeAlias = "TextInput"
NestedComponent = Union[MessageComponent, ModalComponent]
@@ -131,8 +137,6 @@ class ActionRow(Component, Generic[ComponentT]):
Attributes
----------
- type: :class:`ComponentType`
- The type of component.
children: List[Union[:class:`Button`, :class:`BaseSelectMenu`, :class:`TextInput`]]
The children components that this holds, if any.
"""
@@ -142,10 +146,9 @@ class ActionRow(Component, Generic[ComponentT]):
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: ActionRowPayload) -> None:
- self.type: ComponentType = try_enum(ComponentType, data["type"])
- self.children: List[ComponentT] = [
- _component_factory(d) for d in data.get("components", [])
- ]
+ self.type: Literal[ComponentType.action_row] = ComponentType.action_row
+ children = [_component_factory(d) for d in data.get("components", [])]
+ self.children: List[ComponentT] = children # type: ignore
def to_dict(self) -> ActionRowPayload:
return {
@@ -195,7 +198,7 @@ class Button(Component):
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
def __init__(self, data: ButtonComponentPayload) -> None:
- self.type: ComponentType = try_enum(ComponentType, data["type"])
+ self.type: Literal[ComponentType.button] = ComponentType.button
self.style: ButtonStyle = try_enum(ButtonStyle, data["style"])
self.custom_id: Optional[str] = data.get("custom_id")
self.url: Optional[str] = data.get("url")
@@ -209,7 +212,7 @@ def __init__(self, data: ButtonComponentPayload) -> None:
def to_dict(self) -> ButtonComponentPayload:
payload: ButtonComponentPayload = {
- "type": 2,
+ "type": self.type.value,
"style": self.style.value,
"disabled": self.disabled,
}
@@ -273,8 +276,13 @@ class BaseSelectMenu(Component):
__repr_info__: ClassVar[Tuple[str, ...]] = __slots__
- def __init__(self, data: BaseSelectMenuPayload) -> None:
- self.type: ComponentType = try_enum(ComponentType, data["type"])
+ # n.b: ideally this would be `BaseSelectMenuPayload`,
+ # but pyright made TypedDict keys invariant and doesn't
+ # fully support readonly items yet (which would help avoid this)
+ def __init__(self, data: AnySelectMenuPayload) -> None:
+ component_type = try_enum(ComponentType, data["type"])
+ self.type: SelectMenuType = component_type # type: ignore
+
self.custom_id: str = data["custom_id"]
self.placeholder: Optional[str] = data.get("placeholder")
self.min_values: int = data.get("min_values", 1)
@@ -329,6 +337,7 @@ class StringSelectMenu(BaseSelectMenu):
__slots__: Tuple[str, ...] = ("options",)
__repr_info__: ClassVar[Tuple[str, ...]] = BaseSelectMenu.__repr_info__ + __slots__
+ type: Literal[ComponentType.string_select]
def __init__(self, data: StringSelectMenuPayload) -> None:
super().__init__(data)
@@ -372,6 +381,8 @@ class UserSelectMenu(BaseSelectMenu):
__slots__: Tuple[str, ...] = ()
+ type: Literal[ComponentType.user_select]
+
if TYPE_CHECKING:
def to_dict(self) -> UserSelectMenuPayload:
@@ -405,6 +416,8 @@ class RoleSelectMenu(BaseSelectMenu):
__slots__: Tuple[str, ...] = ()
+ type: Literal[ComponentType.role_select]
+
if TYPE_CHECKING:
def to_dict(self) -> RoleSelectMenuPayload:
@@ -438,6 +451,8 @@ class MentionableSelectMenu(BaseSelectMenu):
__slots__: Tuple[str, ...] = ()
+ type: Literal[ComponentType.mentionable_select]
+
if TYPE_CHECKING:
def to_dict(self) -> MentionableSelectMenuPayload:
@@ -475,6 +490,7 @@ class ChannelSelectMenu(BaseSelectMenu):
__slots__: Tuple[str, ...] = ("channel_types",)
__repr_info__: ClassVar[Tuple[str, ...]] = BaseSelectMenu.__repr_info__ + __slots__
+ type: Literal[ComponentType.channel_select]
def __init__(self, data: ChannelSelectMenuPayload) -> None:
super().__init__(data)
@@ -643,7 +659,7 @@ class TextInput(Component):
def __init__(self, data: TextInputPayload) -> None:
style = data.get("style", TextInputStyle.short.value)
- self.type: ComponentType = try_enum(ComponentType, data["type"])
+ self.type: Literal[ComponentType.text_input] = ComponentType.text_input
self.custom_id: str = data["custom_id"]
self.style: TextInputStyle = try_enum(TextInputStyle, style)
self.label: Optional[str] = data.get("label")
diff --git a/disnake/embeds.py b/disnake/embeds.py
index 22ae7398af..1866d8d7eb 100644
--- a/disnake/embeds.py
+++ b/disnake/embeds.py
@@ -112,7 +112,7 @@ class _EmbedAuthorProxy(Sized, Protocol):
class Embed:
"""Represents a Discord embed.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/emoji.py b/disnake/emoji.py
index fb5ee1c3b4..badedbce86 100644
--- a/disnake/emoji.py
+++ b/disnake/emoji.py
@@ -28,7 +28,7 @@ class Emoji(_EmojiTag, AssetMixin):
Depending on the way this object was created, some of the attributes can
have a value of ``None``.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -151,7 +151,7 @@ def roles(self) -> List[Role]:
and count towards a separate limit of 25 emojis.
"""
guild = self.guild
- if guild is None:
+ if guild is None: # pyright: ignore[reportUnnecessaryComparison]
return []
return [role for role in guild.roles if self._roles.has(role.id)]
@@ -192,7 +192,7 @@ async def delete(self, *, reason: Optional[str] = None) -> None:
Raises
------
Forbidden
- You are not allowed to delete emojis.
+ You are not allowed to delete this emoji.
HTTPException
An error occurred deleting the emoji.
"""
@@ -227,7 +227,7 @@ async def edit(
Raises
------
Forbidden
- You are not allowed to edit emojis.
+ You are not allowed to edit this emoji.
HTTPException
An error occurred editing the emoji.
diff --git a/disnake/enums.py b/disnake/enums.py
index 29835a8715..912cb36183 100644
--- a/disnake/enums.py
+++ b/disnake/enums.py
@@ -35,6 +35,7 @@
"ActivityType",
"NotificationLevel",
"TeamMembershipState",
+ "TeamMemberRole",
"WebhookType",
"ExpireBehaviour",
"ExpireBehavior",
@@ -466,7 +467,7 @@ def category(self) -> Optional[AuditLogActionCategory]:
@property
def target_type(self) -> Optional[str]:
v = self.value
- if v == -1:
+ if v == -1: # pyright: ignore[reportUnnecessaryComparison]
return "all"
elif v < 10:
return "guild"
@@ -551,6 +552,15 @@ class TeamMembershipState(Enum):
accepted = 2
+class TeamMemberRole(Enum):
+ admin = "admin"
+ developer = "developer"
+ read_only = "read_only"
+
+ def __str__(self) -> str:
+ return self.name
+
+
class WebhookType(Enum):
incoming = 1
channel_follower = 2
@@ -627,7 +637,7 @@ class ComponentType(Enum):
action_row = 1
button = 2
string_select = 3
- select = string_select # backwards compatibility
+ select = 3 # backwards compatibility
text_input = 4
user_select = 5
role_select = 6
@@ -753,7 +763,7 @@ def __str__(self) -> str:
# reference: https://discord.com/developers/docs/reference#locales
class Locale(Enum):
bg = "bg"
- "Bulgarian | български" # noqa: RUF001
+ "Bulgarian | български"
cs = "cs"
"Czech | Čeština"
da = "da"
@@ -761,7 +771,7 @@ class Locale(Enum):
de = "de"
"German | Deutsch"
el = "el"
- "Greek | Ελληνικά" # noqa: RUF001
+ "Greek | Ελληνικά"
en_GB = "en-GB"
"English, UK | English, UK"
en_US = "en-US"
@@ -807,7 +817,7 @@ class Locale(Enum):
tr = "tr"
"Turkish | Türkçe"
uk = "uk"
- "Ukrainian | Українська" # noqa: RUF001
+ "Ukrainian | Українська"
vi = "vi"
"Vietnamese | Tiếng Việt"
zh_CN = "zh-CN"
diff --git a/disnake/errors.py b/disnake/errors.py
index 21a1834dff..416a32d7f1 100644
--- a/disnake/errors.py
+++ b/disnake/errors.py
@@ -2,7 +2,7 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, ClassVar, Dict, List, Mapping, Optional, Tuple, Union
if TYPE_CHECKING:
from aiohttp import ClientResponse, ClientWebSocketResponse
@@ -225,7 +225,7 @@ class ConnectionClosed(ClientException):
"""
# https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes
- GATEWAY_CLOSE_EVENT_REASONS: Dict[int, str] = {
+ GATEWAY_CLOSE_EVENT_REASONS: ClassVar[Mapping[int, str]] = {
4000: "Unknown error",
4001: "Unknown opcode",
4002: "Decode error",
@@ -243,7 +243,7 @@ class ConnectionClosed(ClientException):
}
# https://discord.com/developers/docs/topics/opcodes-and-status-codes#voice-voice-close-event-codes
- GATEWAY_VOICE_CLOSE_EVENT_REASONS: Dict[int, str] = {
+ GATEWAY_VOICE_CLOSE_EVENT_REASONS: ClassVar[Mapping[int, str]] = {
**GATEWAY_CLOSE_EVENT_REASONS,
4002: "Failed to decode payload",
4006: "Session no longer valid",
diff --git a/disnake/ext/commands/_types.py b/disnake/ext/commands/_types.py
index eb90ee0a42..c6b0a26ece 100644
--- a/disnake/ext/commands/_types.py
+++ b/disnake/ext/commands/_types.py
@@ -3,6 +3,8 @@
from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar, Union
if TYPE_CHECKING:
+ from disnake import ApplicationCommandInteraction
+
from .cog import Cog
from .context import Context
from .errors import CommandError
@@ -16,6 +18,10 @@
Check = Union[
Callable[["Cog", "Context[Any]"], MaybeCoro[bool]], Callable[["Context[Any]"], MaybeCoro[bool]]
]
+AppCheck = Union[
+ Callable[["Cog", "ApplicationCommandInteraction"], MaybeCoro[bool]],
+ Callable[["ApplicationCommandInteraction"], MaybeCoro[bool]],
+]
Hook = Union[Callable[["Cog", "Context[Any]"], Coro[Any]], Callable[["Context[Any]"], Coro[Any]]]
Error = Union[
Callable[["Cog", "Context[Any]", "CommandError"], Coro[Any]],
diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py
index c21cc5f2d1..b5e0498399 100644
--- a/disnake/ext/commands/base_core.py
+++ b/disnake/ext/commands/base_core.py
@@ -33,7 +33,7 @@
from disnake.interactions import ApplicationCommandInteraction
- from ._types import Check, Coro, Error, Hook
+ from ._types import AppCheck, Coro, Error, Hook
from .cog import Cog
ApplicationCommandInteractionT = TypeVar(
@@ -155,7 +155,7 @@ def __init__(self, func: CommandCallback, *, name: Optional[str] = None, **kwarg
except AttributeError:
checks = kwargs.get("checks", [])
- self.checks: List[Check] = checks
+ self.checks: List[AppCheck] = checks
try:
cooldown = func.__commands_cooldown__
@@ -253,10 +253,10 @@ def default_member_permissions(self) -> Optional[Permissions]:
def callback(self) -> CommandCallback:
return self._callback
- def add_check(self, func: Check) -> None:
+ def add_check(self, func: AppCheck) -> None:
"""Adds a check to the application command.
- This is the non-decorator interface to :func:`.check`.
+ This is the non-decorator interface to :func:`.app_check`.
Parameters
----------
@@ -265,7 +265,7 @@ def add_check(self, func: Check) -> None:
"""
self.checks.append(func)
- def remove_check(self, func: Check) -> None:
+ def remove_check(self, func: AppCheck) -> None:
"""Removes a check from the application command.
This function is idempotent and will not raise an exception
@@ -303,7 +303,7 @@ def _prepare_cooldowns(self, inter: ApplicationCommandInteraction) -> None:
dt = inter.created_at
current = dt.replace(tzinfo=datetime.timezone.utc).timestamp()
bucket = self._buckets.get_bucket(inter, current) # type: ignore
- if bucket is not None:
+ if bucket is not None: # pyright: ignore[reportUnnecessaryComparison]
retry_after = bucket.update_rate_limit(current)
if retry_after:
raise CommandOnCooldown(bucket, retry_after, self._buckets.type) # type: ignore
@@ -636,7 +636,9 @@ def default_member_permissions(
ban_members: bool = ...,
change_nickname: bool = ...,
connect: bool = ...,
+ create_events: bool = ...,
create_forum_threads: bool = ...,
+ create_guild_expressions: bool = ...,
create_instant_invite: bool = ...,
create_private_threads: bool = ...,
create_public_threads: bool = ...,
diff --git a/disnake/ext/commands/bot.py b/disnake/ext/commands/bot.py
index 5c3ba59eac..825f96e6ae 100644
--- a/disnake/ext/commands/bot.py
+++ b/disnake/ext/commands/bot.py
@@ -184,7 +184,7 @@ class Bot(BotBase, InteractionBotBase, disnake.Client):
owner_ids: Optional[Collection[:class:`int`]]
The IDs of the users that own the bot. This is similar to :attr:`owner_id`.
If this is not set and the application is team based, then it is
- fetched automatically using :meth:`~.Bot.application_info`.
+ fetched automatically using :meth:`~.Bot.application_info` (taking team roles into account).
For performance reasons it is recommended to use a :class:`set`
for the collection. You cannot set both ``owner_id`` and ``owner_ids``.
@@ -403,7 +403,7 @@ class InteractionBot(InteractionBotBase, disnake.Client):
owner_ids: Optional[Collection[:class:`int`]]
The IDs of the users that own the bot. This is similar to :attr:`owner_id`.
If this is not set and the application is team based, then it is
- fetched automatically using :meth:`~.Bot.application_info`.
+ fetched automatically using :meth:`~.Bot.application_info` (taking team roles into account).
For performance reasons it is recommended to use a :class:`set`
for the collection. You cannot set both ``owner_id`` and ``owner_ids``.
diff --git a/disnake/ext/commands/bot_base.py b/disnake/ext/commands/bot_base.py
index d55dc63490..1bba906c82 100644
--- a/disnake/ext/commands/bot_base.py
+++ b/disnake/ext/commands/bot_base.py
@@ -10,18 +10,7 @@
import sys
import traceback
import warnings
-from typing import (
- TYPE_CHECKING,
- Any,
- Callable,
- Iterable,
- List,
- Optional,
- Type,
- TypeVar,
- Union,
- cast,
-)
+from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Type, TypeVar, Union
import disnake
@@ -414,7 +403,7 @@ def _remove_module_references(self, name: str) -> None:
super()._remove_module_references(name)
# remove all the commands from the module
for cmd in self.all_commands.copy().values():
- if cmd.module is not None and _is_submodule(name, cmd.module):
+ if cmd.module and _is_submodule(name, cmd.module):
if isinstance(cmd, GroupMixin):
cmd.recursively_remove_all_commands()
self.remove_command(cmd.name)
@@ -513,7 +502,7 @@ class be provided, it must be similar enough to :class:`.Context`\'s
``cls`` parameter.
"""
view = StringView(message.content)
- ctx = cast("CXT", cls(prefix=None, view=view, bot=self, message=message))
+ ctx = cls(prefix=None, view=view, bot=self, message=message)
if message.author.id == self.user.id: # type: ignore
return ctx
diff --git a/disnake/ext/commands/cog.py b/disnake/ext/commands/cog.py
index a0305ccea7..01fd59937c 100644
--- a/disnake/ext/commands/cog.py
+++ b/disnake/ext/commands/cog.py
@@ -789,30 +789,30 @@ def _inject(self, bot: AnyBot) -> Self:
# Add application command checks
if cls.bot_slash_command_check is not Cog.bot_slash_command_check:
- bot.add_app_command_check(self.bot_slash_command_check, slash_commands=True) # type: ignore
+ bot.add_app_command_check(self.bot_slash_command_check, slash_commands=True)
if cls.bot_user_command_check is not Cog.bot_user_command_check:
- bot.add_app_command_check(self.bot_user_command_check, user_commands=True) # type: ignore
+ bot.add_app_command_check(self.bot_user_command_check, user_commands=True)
if cls.bot_message_command_check is not Cog.bot_message_command_check:
- bot.add_app_command_check(self.bot_message_command_check, message_commands=True) # type: ignore
+ bot.add_app_command_check(self.bot_message_command_check, message_commands=True)
# Add app command one-off checks
if cls.bot_slash_command_check_once is not Cog.bot_slash_command_check_once:
bot.add_app_command_check(
- self.bot_slash_command_check_once, # type: ignore
+ self.bot_slash_command_check_once,
call_once=True,
slash_commands=True,
)
if cls.bot_user_command_check_once is not Cog.bot_user_command_check_once:
bot.add_app_command_check(
- self.bot_user_command_check_once, call_once=True, user_commands=True # type: ignore
+ self.bot_user_command_check_once, call_once=True, user_commands=True
)
if cls.bot_message_command_check_once is not Cog.bot_message_command_check_once:
bot.add_app_command_check(
- self.bot_message_command_check_once, # type: ignore
+ self.bot_message_command_check_once,
call_once=True,
message_commands=True,
)
@@ -859,32 +859,32 @@ def _eject(self, bot: AnyBot) -> None:
# Remove application command checks
if cls.bot_slash_command_check is not Cog.bot_slash_command_check:
- bot.remove_app_command_check(self.bot_slash_command_check, slash_commands=True) # type: ignore
+ bot.remove_app_command_check(self.bot_slash_command_check, slash_commands=True)
if cls.bot_user_command_check is not Cog.bot_user_command_check:
- bot.remove_app_command_check(self.bot_user_command_check, user_commands=True) # type: ignore
+ bot.remove_app_command_check(self.bot_user_command_check, user_commands=True)
if cls.bot_message_command_check is not Cog.bot_message_command_check:
- bot.remove_app_command_check(self.bot_message_command_check, message_commands=True) # type: ignore
+ bot.remove_app_command_check(self.bot_message_command_check, message_commands=True)
# Remove app command one-off checks
if cls.bot_slash_command_check_once is not Cog.bot_slash_command_check_once:
bot.remove_app_command_check(
- self.bot_slash_command_check_once, # type: ignore
+ self.bot_slash_command_check_once,
call_once=True,
slash_commands=True,
)
if cls.bot_user_command_check_once is not Cog.bot_user_command_check_once:
bot.remove_app_command_check(
- self.bot_user_command_check_once, # type: ignore
+ self.bot_user_command_check_once,
call_once=True,
user_commands=True,
)
if cls.bot_message_command_check_once is not Cog.bot_message_command_check_once:
bot.remove_app_command_check(
- self.bot_message_command_check_once, # type: ignore
+ self.bot_message_command_check_once,
call_once=True,
message_commands=True,
)
diff --git a/disnake/ext/commands/common_bot_base.py b/disnake/ext/commands/common_bot_base.py
index 841c3df837..737736c170 100644
--- a/disnake/ext/commands/common_bot_base.py
+++ b/disnake/ext/commands/common_bot_base.py
@@ -4,6 +4,7 @@
import asyncio
import collections.abc
+import importlib.machinery
import importlib.util
import logging
import os
@@ -19,8 +20,6 @@
from .cog import Cog
if TYPE_CHECKING:
- import importlib.machinery
-
from ._types import CoroFunc
from .bot import AutoShardedBot, AutoShardedInteractionBot, Bot, InteractionBot
from .help import HelpCommand
@@ -82,8 +81,13 @@ async def _fill_owners(self) -> None:
app: disnake.AppInfo = await self.application_info() # type: ignore
if app.team:
- self.owners = set(app.team.members)
- self.owner_ids = {m.id for m in app.team.members}
+ self.owners = owners = {
+ member
+ for member in app.team.members
+ # these roles can access the bot token, consider them bot owners
+ if member.role in (disnake.TeamMemberRole.admin, disnake.TeamMemberRole.developer)
+ }
+ self.owner_ids = {m.id for m in owners}
else:
self.owner = app.owner
self.owner_id = app.owner.id
@@ -131,6 +135,10 @@ async def is_owner(self, user: Union[disnake.User, disnake.Member]) -> bool:
The function also checks if the application is team-owned if
:attr:`owner_ids` is not set.
+ .. versionchanged:: 2.10
+ Also takes team roles into account; only team members with the :attr:`~disnake.TeamMemberRole.admin`
+ or :attr:`~disnake.TeamMemberRole.developer` roles are considered bot owners.
+
Parameters
----------
user: :class:`.abc.User`
diff --git a/disnake/ext/commands/context.py b/disnake/ext/commands/context.py
index 5c25bdc9dc..0876e7d3a4 100644
--- a/disnake/ext/commands/context.py
+++ b/disnake/ext/commands/context.py
@@ -290,9 +290,7 @@ def voice_client(self) -> Optional[VoiceProtocol]:
return g.voice_client if g else None
async def send_help(self, *args: Any) -> Any:
- """send_help(entity=)
-
- |coro|
+ """|coro|
Shows the help command for the specified entity if given.
The entity can be a command or a cog.
diff --git a/disnake/ext/commands/converter.py b/disnake/ext/commands/converter.py
index 8bca2bd6dd..29672b2e54 100644
--- a/disnake/ext/commands/converter.py
+++ b/disnake/ext/commands/converter.py
@@ -1133,7 +1133,7 @@ def __class_getitem__(cls, params: Union[Tuple[T], T]) -> Greedy[T]:
raise TypeError("Greedy[...] expects a type or a Converter instance.")
if converter in (str, type(None)) or origin is Greedy:
- raise TypeError(f"Greedy[{converter.__name__}] is invalid.") # type: ignore
+ raise TypeError(f"Greedy[{converter.__name__}] is invalid.")
if origin is Union and type(None) in args:
raise TypeError(f"Greedy[{converter!r}] is invalid.")
@@ -1161,7 +1161,7 @@ def get_converter(param: inspect.Parameter) -> Any:
return converter
-_GenericAlias = type(List[T])
+_GenericAlias = type(List[Any])
def is_generic_type(tp: Any, *, _GenericAlias: Type = _GenericAlias) -> bool:
@@ -1222,7 +1222,7 @@ async def _actual_conversion(
raise ConversionError(converter, exc) from exc
try:
- return converter(argument)
+ return converter(argument) # type: ignore
except CommandError:
raise
except Exception as exc:
diff --git a/disnake/ext/commands/cooldowns.py b/disnake/ext/commands/cooldowns.py
index 4268f76fff..354754550a 100644
--- a/disnake/ext/commands/cooldowns.py
+++ b/disnake/ext/commands/cooldowns.py
@@ -228,7 +228,7 @@ def get_bucket(self, message: Message, current: Optional[float] = None) -> Coold
key = self._bucket_key(message)
if key not in self._cache:
bucket = self.create_bucket(message)
- if bucket is not None:
+ if bucket is not None: # pyright: ignore[reportUnnecessaryComparison]
self._cache[key] = bucket
else:
bucket = self._cache[key]
diff --git a/disnake/ext/commands/core.py b/disnake/ext/commands/core.py
index 2d7ff5497e..b9d49ab269 100644
--- a/disnake/ext/commands/core.py
+++ b/disnake/ext/commands/core.py
@@ -27,7 +27,12 @@
)
import disnake
-from disnake.utils import _generated, _overload_with_permissions
+from disnake.utils import (
+ _generated,
+ _overload_with_permissions,
+ get_signature_parameters,
+ unwrap_function,
+)
from ._types import _BaseCommand
from .cog import Cog
@@ -62,7 +67,7 @@
from disnake.message import Message
- from ._types import Check, Coro, CoroFunc, Error, Hook
+ from ._types import AppCheck, Check, Coro, CoroFunc, Error, Hook
__all__ = (
@@ -76,6 +81,8 @@
"has_any_role",
"check",
"check_any",
+ "app_check",
+ "app_check_any",
"before_invoke",
"after_invoke",
"bot_has_role",
@@ -114,42 +121,6 @@
P = TypeVar("P")
-def unwrap_function(function: Callable[..., Any]) -> Callable[..., Any]:
- partial = functools.partial
- while True:
- if hasattr(function, "__wrapped__"):
- function = function.__wrapped__
- elif isinstance(function, partial):
- function = function.func
- else:
- return function
-
-
-def get_signature_parameters(
- function: Callable[..., Any], globalns: Dict[str, Any]
-) -> Dict[str, inspect.Parameter]:
- signature = inspect.signature(function)
- params = {}
- cache: Dict[str, Any] = {}
- eval_annotation = disnake.utils.evaluate_annotation
- for name, parameter in signature.parameters.items():
- annotation = parameter.annotation
- if annotation is parameter.empty:
- params[name] = parameter
- continue
- if annotation is None:
- params[name] = parameter.replace(annotation=type(None))
- continue
-
- annotation = eval_annotation(annotation, globalns, globalns, cache)
- if annotation is Greedy:
- raise TypeError("Unparameterized Greedy[...] is disallowed in signature.")
-
- params[name] = parameter.replace(annotation=annotation)
-
- return params
-
-
def wrap_callback(coro):
@functools.wraps(coro)
async def wrapped(*args, **kwargs):
@@ -410,7 +381,11 @@ def callback(self, function: CommandCallback[CogT, Any, P, T]) -> None:
except AttributeError:
globalns = {}
- self.params = get_signature_parameters(function, globalns)
+ params = get_signature_parameters(function, globalns, skip_standard_params=True)
+ for param in params.values():
+ if param.annotation is Greedy:
+ raise TypeError("Unparameterized Greedy[...] is disallowed in signature.")
+ self.params = params
def add_check(self, func: Check) -> None:
"""Adds a check to the command.
@@ -632,21 +607,7 @@ def clean_params(self) -> Dict[str, inspect.Parameter]:
Useful for inspecting signature.
"""
- result = self.params.copy()
- if self.cog is not None:
- # first parameter is self
- try:
- del result[next(iter(result))]
- except StopIteration:
- raise ValueError("missing 'self' parameter") from None
-
- try:
- # first/second parameter is context
- del result[next(iter(result))]
- except StopIteration:
- raise ValueError("missing 'context' parameter") from None
-
- return result
+ return self.params.copy()
@property
def full_parent_name(self) -> str:
@@ -718,27 +679,7 @@ async def _parse_arguments(self, ctx: Context) -> None:
kwargs = ctx.kwargs
view = ctx.view
- iterator = iter(self.params.items())
-
- if self.cog is not None:
- # we have 'self' as the first parameter so just advance
- # the iterator and resume parsing
- try:
- next(iterator)
- except StopIteration:
- raise disnake.ClientException(
- f'Callback for {self.name} command is missing "self" parameter.'
- ) from None
-
- # next we have the 'ctx' as the next parameter
- try:
- next(iterator)
- except StopIteration:
- raise disnake.ClientException(
- f'Callback for {self.name} command is missing "ctx" parameter.'
- ) from None
-
- for name, param in iterator:
+ for name, param in self.params.items():
ctx.current_parameter = param
if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY):
transformed = await self.transform(ctx, param)
@@ -814,7 +755,7 @@ def _prepare_cooldowns(self, ctx: Context) -> None:
dt = ctx.message.edited_at or ctx.message.created_at
current = dt.replace(tzinfo=datetime.timezone.utc).timestamp()
bucket = self._buckets.get_bucket(ctx.message, current)
- if bucket is not None:
+ if bucket is not None: # pyright: ignore[reportUnnecessaryComparison]
retry_after = bucket.update_rate_limit(current)
if retry_after:
raise CommandOnCooldown(bucket, retry_after, self._buckets.type) # type: ignore
@@ -1722,6 +1663,9 @@ async def extended_check(ctx):
The function returned by ``predicate`` is **always** a coroutine,
even if the original function was not a coroutine.
+ .. note::
+ See :func:`.app_check` for this function's application command counterpart.
+
.. versionchanged:: 1.3
The ``predicate`` attribute was added.
@@ -1774,7 +1718,7 @@ def decorator(func: Union[Command, CoroFunc]) -> Union[Command, CoroFunc]:
decorator.predicate = predicate
else:
- @functools.wraps(predicate)
+ @functools.wraps(predicate) # type: ignore
async def wrapper(ctx):
return predicate(ctx) # type: ignore
@@ -1794,6 +1738,9 @@ def check_any(*checks: Check) -> Callable[[T], T]:
The ``predicate`` attribute for this function **is** a coroutine.
+ .. note::
+ See :func:`.app_check_any` for this function's application command counterpart.
+
.. versionadded:: 1.3
Parameters
@@ -1850,6 +1797,46 @@ async def predicate(ctx: AnyContext) -> bool:
return check(predicate)
+def app_check(predicate: AppCheck) -> Callable[[T], T]:
+ """Same as :func:`.check`, but for app commands.
+
+ .. versionadded:: 2.10
+
+ Parameters
+ ----------
+ predicate: Callable[[:class:`disnake.ApplicationCommandInteraction`], :class:`bool`]
+ The predicate to check if the command should be invoked.
+ """
+ return check(predicate) # type: ignore # impl is the same, typings are different
+
+
+def app_check_any(*checks: AppCheck) -> Callable[[T], T]:
+ """Same as :func:`.check_any`, but for app commands.
+
+ .. note::
+ See :func:`.check_any` for this function's prefix command counterpart.
+
+ .. versionadded:: 2.10
+
+ Parameters
+ ----------
+ *checks: Callable[[:class:`disnake.ApplicationCommandInteraction`], :class:`bool`]
+ An argument list of checks that have been decorated with
+ the :func:`app_check` decorator.
+
+ Raises
+ ------
+ TypeError
+ A check passed has not been decorated with the :func:`app_check`
+ decorator.
+ """
+ try:
+ return check_any(*checks) # type: ignore # impl is the same, typings are different
+ except TypeError as e:
+ msg = str(e).replace("commands.check", "commands.app_check") # fix err message
+ raise TypeError(msg) from None
+
+
def has_role(item: Union[int, str]) -> Callable[[T], T]:
"""A :func:`.check` that is added that checks if the member invoking the
command has the role specified via the name or ID specified.
@@ -2012,7 +1999,9 @@ def has_permissions(
ban_members: bool = ...,
change_nickname: bool = ...,
connect: bool = ...,
+ create_events: bool = ...,
create_forum_threads: bool = ...,
+ create_guild_expressions: bool = ...,
create_instant_invite: bool = ...,
create_private_threads: bool = ...,
create_public_threads: bool = ...,
@@ -2134,7 +2123,9 @@ def bot_has_permissions(
ban_members: bool = ...,
change_nickname: bool = ...,
connect: bool = ...,
+ create_events: bool = ...,
create_forum_threads: bool = ...,
+ create_guild_expressions: bool = ...,
create_instant_invite: bool = ...,
create_private_threads: bool = ...,
create_public_threads: bool = ...,
@@ -2234,7 +2225,9 @@ def has_guild_permissions(
ban_members: bool = ...,
change_nickname: bool = ...,
connect: bool = ...,
+ create_events: bool = ...,
create_forum_threads: bool = ...,
+ create_guild_expressions: bool = ...,
create_instant_invite: bool = ...,
create_private_threads: bool = ...,
create_public_threads: bool = ...,
@@ -2331,7 +2324,9 @@ def bot_has_guild_permissions(
ban_members: bool = ...,
change_nickname: bool = ...,
connect: bool = ...,
+ create_events: bool = ...,
create_forum_threads: bool = ...,
+ create_guild_expressions: bool = ...,
create_instant_invite: bool = ...,
create_private_threads: bool = ...,
create_public_threads: bool = ...,
diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py
index 25329d9305..cfa4c12f03 100644
--- a/disnake/ext/commands/errors.py
+++ b/disnake/ext/commands/errors.py
@@ -14,7 +14,7 @@
from disnake.threads import Thread
from disnake.types.snowflake import Snowflake, SnowflakeList
- from .context import Context
+ from .context import AnyContext
from .cooldowns import BucketType, Cooldown
from .flag_converter import Flag
@@ -181,7 +181,8 @@ class BadArgument(UserInputError):
class CheckFailure(CommandError):
- """Exception raised when the predicates in :attr:`.Command.checks` have failed.
+ """Exception raised when the predicates in :attr:`.Command.checks` or
+ :attr:`.InvokableApplicationCommand.checks` have failed.
This inherits from :exc:`CommandError`
"""
@@ -190,7 +191,7 @@ class CheckFailure(CommandError):
class CheckAnyFailure(CheckFailure):
- """Exception raised when all predicates in :func:`check_any` fail.
+ """Exception raised when all predicates in :func:`check_any` or :func:`app_check_any` fail.
This inherits from :exc:`CheckFailure`.
@@ -200,13 +201,15 @@ class CheckAnyFailure(CheckFailure):
----------
errors: List[:class:`CheckFailure`]
A list of errors that were caught during execution.
- checks: List[Callable[[:class:`Context`], :class:`bool`]]
+ checks: List[Callable[[Union[:class:`Context`, :class:`disnake.ApplicationCommandInteraction`]], :class:`bool`]]
A list of check predicates that failed.
"""
- def __init__(self, checks: List[CheckFailure], errors: List[Callable[[Context], bool]]) -> None:
+ def __init__(
+ self, checks: List[CheckFailure], errors: List[Callable[[AnyContext], bool]]
+ ) -> None:
self.checks: List[CheckFailure] = checks
- self.errors: List[Callable[[Context], bool]] = errors
+ self.errors: List[Callable[[AnyContext], bool]] = errors
super().__init__("You do not have permission to run this command.")
diff --git a/disnake/ext/commands/flag_converter.py b/disnake/ext/commands/flag_converter.py
index 39a4b54808..37c97936c0 100644
--- a/disnake/ext/commands/flag_converter.py
+++ b/disnake/ext/commands/flag_converter.py
@@ -435,7 +435,7 @@ class FlagConverter(metaclass=FlagsMeta):
how this converter works, check the appropriate
:ref:`documentation `.
- .. container:: operations
+ .. collapse:: operations
.. describe:: iter(x)
diff --git a/disnake/ext/commands/flags.py b/disnake/ext/commands/flags.py
index ade3e79182..866566af3b 100644
--- a/disnake/ext/commands/flags.py
+++ b/disnake/ext/commands/flags.py
@@ -25,7 +25,7 @@ class CommandSyncFlags(BaseFlags):
.. versionadded:: 2.7
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/ext/commands/help.py b/disnake/ext/commands/help.py
index 54bcb1b43a..ecd3988b86 100644
--- a/disnake/ext/commands/help.py
+++ b/disnake/ext/commands/help.py
@@ -5,7 +5,7 @@
import functools
import itertools
import re
-from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional
+from typing import TYPE_CHECKING, Any, Callable, ClassVar, Iterable, Mapping, Optional
import disnake.utils
@@ -48,7 +48,7 @@
class Paginator:
"""A class that aids in paginating code blocks for Discord messages.
- .. container:: operations
+ .. collapse:: operations
.. describe:: len(x)
@@ -202,16 +202,6 @@ async def _parse_arguments(self, ctx) -> None:
async def _on_error_cog_implementation(self, dummy, ctx, error) -> None:
await self._injected.on_help_command_error(ctx, error)
- @property
- def clean_params(self):
- result = self.params.copy()
- try:
- del result[next(iter(result))]
- except StopIteration:
- raise ValueError("Missing context parameter") from None
- else:
- return result
-
def _inject_into_cog(self, cog) -> None:
# Warning: hacky
@@ -279,7 +269,7 @@ class HelpCommand:
ones passed in the :class:`.Command` constructor.
"""
- MENTION_TRANSFORMS = {
+ MENTION_TRANSFORMS: ClassVar[Mapping[str, str]] = {
"@everyone": "@\u200beveryone",
"@here": "@\u200bhere",
r"<@!?[0-9]{17,19}>": "@deleted-user",
@@ -378,7 +368,11 @@ def invoked_with(self):
"""
command_name = self._command_impl.name
ctx = self.context
- if ctx is None or ctx.command is None or ctx.command.qualified_name != command_name:
+ if (
+ ctx is disnake.utils.MISSING
+ or ctx.command is None
+ or ctx.command.qualified_name != command_name
+ ):
return command_name
return ctx.invoked_with
diff --git a/disnake/ext/commands/interaction_bot_base.py b/disnake/ext/commands/interaction_bot_base.py
index 25308c3649..5349abeb3e 100644
--- a/disnake/ext/commands/interaction_bot_base.py
+++ b/disnake/ext/commands/interaction_bot_base.py
@@ -54,7 +54,7 @@
)
from disnake.permissions import Permissions
- from ._types import Check, CoroFunc
+ from ._types import AppCheck, CoroFunc
from .base_core import CogT, CommandCallback, InteractionCommandCallback
P = ParamSpec("P")
@@ -991,7 +991,7 @@ async def on_message_command_error(
def add_app_command_check(
self,
- func: Check,
+ func: AppCheck,
*,
call_once: bool = False,
slash_commands: bool = False,
@@ -1000,8 +1000,8 @@ def add_app_command_check(
) -> None:
"""Adds a global application command check to the bot.
- This is the non-decorator interface to :meth:`.check`,
- :meth:`.check_once`, :meth:`.slash_command_check` and etc.
+ This is the non-decorator interface to :func:`.app_check`,
+ :meth:`.slash_command_check` and etc.
You must specify at least one of the bool parameters, otherwise
the check won't be added.
@@ -1039,7 +1039,7 @@ def add_app_command_check(
def remove_app_command_check(
self,
- func: Check,
+ func: AppCheck,
*,
call_once: bool = False,
slash_commands: bool = False,
@@ -1060,7 +1060,7 @@ def remove_app_command_check(
The function to remove from the global checks.
call_once: :class:`bool`
Whether the function was added with ``call_once=True`` in
- the :meth:`.Bot.add_check` call or using :meth:`.check_once`.
+ the :meth:`.Bot.add_app_command_check` call.
slash_commands: :class:`bool`
Whether this check was for slash commands.
user_commands: :class:`bool`
@@ -1179,7 +1179,7 @@ def decorator(
) -> Callable[[ApplicationCommandInteraction], Any]:
# T was used instead of Check to ensure the type matches on return
self.add_app_command_check(
- func, # type: ignore
+ func,
call_once=call_once,
slash_commands=slash_commands,
user_commands=user_commands,
diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py
index 2ab93359d2..e472c1ae13 100644
--- a/disnake/ext/commands/params.py
+++ b/disnake/ext/commands/params.py
@@ -6,10 +6,12 @@
import asyncio
import collections.abc
+import copy
import inspect
import itertools
import math
import sys
+import types
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum, EnumMeta
@@ -31,9 +33,7 @@
Type,
TypeVar,
Union,
- get_args,
get_origin,
- get_type_hints,
)
import disnake
@@ -43,7 +43,12 @@
from disnake.ext import commands
from disnake.i18n import Localized
from disnake.interactions import ApplicationCommandInteraction
-from disnake.utils import maybe_coroutine
+from disnake.utils import (
+ get_signature_parameters,
+ get_signature_return,
+ maybe_coroutine,
+ signature_has_self_param,
+)
from . import errors
from .converter import CONVERTER_MAPPING
@@ -81,9 +86,6 @@
if sys.version_info >= (3, 10):
from types import EllipsisType, UnionType
-elif TYPE_CHECKING:
- UnionType = object()
- EllipsisType = ellipsis # noqa: F821
else:
UnionType = object()
EllipsisType = type(Ellipsis)
@@ -110,17 +112,26 @@
def issubclass_(obj: Any, tp: Union[TypeT, Tuple[TypeT, ...]]) -> TypeGuard[TypeT]:
+ """Similar to the builtin `issubclass`, but more lenient.
+ Can also handle unions (`issubclass(Union[int, str], int)`) and
+ generic types (`issubclass(X[T], X)`) in the first argument.
+ """
if not isinstance(tp, (type, tuple)):
return False
- elif not isinstance(obj, type):
- # Assume we have a type hint
- if get_origin(obj) in (Union, UnionType, Optional):
- obj = get_args(obj)
- return any(isinstance(o, type) and issubclass(o, tp) for o in obj)
- else:
- # Other type hint specializations are not supported
- return False
- return issubclass(obj, tp)
+ elif isinstance(obj, type):
+ # common case
+ return issubclass(obj, tp)
+
+ # At this point, `obj` is likely a generic type hint
+ if (origin := get_origin(obj)) is None:
+ return False
+
+ if origin in (Union, UnionType):
+ # If we have a Union, try matching any of its args
+ # (recursively, to handle possibly generic types inside this union)
+ return any(issubclass_(o, tp) for o in obj.__args__)
+ else:
+ return isinstance(origin, type) and issubclass(origin, tp)
def remove_optionals(annotation: Any) -> Any:
@@ -135,37 +146,6 @@ def remove_optionals(annotation: Any) -> Any:
return annotation
-def signature(func: Callable) -> inspect.Signature:
- """Get the signature with evaluated annotations wherever possible
-
- This is equivalent to `signature(..., eval_str=True)` in python 3.10
- """
- if sys.version_info >= (3, 10):
- return inspect.signature(func, eval_str=True)
-
- if inspect.isfunction(func) or inspect.ismethod(func):
- typehints = get_type_hints(func)
- else:
- typehints = get_type_hints(func.__call__)
-
- signature = inspect.signature(func)
- parameters = []
-
- for name, param in signature.parameters.items():
- if isinstance(param.annotation, str):
- param = param.replace(annotation=typehints.get(name, inspect.Parameter.empty))
- if param.annotation is type(None):
- param = param.replace(annotation=None)
-
- parameters.append(param)
-
- return_annotation = typehints.get("return", inspect.Parameter.empty)
- if return_annotation is type(None):
- return_annotation = None
-
- return signature.replace(parameters=parameters, return_annotation=return_annotation)
-
-
def _xt_to_xe(xe: Optional[float], xt: Optional[float], direction: float = 1) -> Optional[float]:
"""Function for combining xt and xe
@@ -472,8 +452,8 @@ class ParamInfo:
.. versionchanged:: 2.5
Added support for localizations.
- choices: Union[List[:class:`.OptionChoice`], List[Union[:class:`str`, :class:`int`]], Dict[:class:`str`, Union[:class:`str`, :class:`int`]]]
- The list of choices of this slash command option.
+ choices: Union[Sequence[:class:`.OptionChoice`], Sequence[Union[:class:`str`, :class:`int`, :class:`float`]], Mapping[:class:`str`, Union[:class:`str`, :class:`int`, :class:`float`]]]
+ The pre-defined choices for this option.
ge: :class:`float`
The lowest allowed value for this option.
le: :class:`float`
@@ -561,7 +541,7 @@ def __init__(
self.max_length = max_length
self.large = large
- def copy(self) -> ParamInfo:
+ def copy(self) -> Self:
# n. b. this method needs to be manually updated when a new attribute is added.
cls = self.__class__
ins = cls.__new__(cls)
@@ -575,7 +555,7 @@ def copy(self) -> ParamInfo:
ins.converter = self.converter
ins.convert_default = self.convert_default
ins.autocomplete = self.autocomplete
- ins.choices = self.choices.copy()
+ ins.choices = copy.copy(self.choices)
ins.type = self.type
ins.channel_types = self.channel_types.copy()
ins.max_value = self.max_value
@@ -787,7 +767,14 @@ def parse_annotation(self, annotation: Any, converter_mode: bool = False) -> boo
return True
def parse_converter_annotation(self, converter: Callable, fallback_annotation: Any) -> None:
- _, parameters = isolate_self(signature(converter))
+ if isinstance(converter, (types.FunctionType, types.MethodType)):
+ converter_func = converter
+ else:
+ # if converter isn't a function/method, assume it's a callable object/type
+ # (we need `__call__` here to get the correct global namespace later, since
+ # classes do not have `__globals__`)
+ converter_func = converter.__call__
+ _, parameters = isolate_self(converter_func)
if len(parameters) != 1:
raise TypeError(
@@ -850,9 +837,9 @@ def to_option(self) -> Option:
def safe_call(function: Callable[..., T], /, *possible_args: Any, **possible_kwargs: Any) -> T:
"""Calls a function without providing any extra unexpected arguments"""
MISSING: Any = object()
- sig = signature(function)
+ parameters = get_signature_parameters(function)
- kinds = {p.kind for p in sig.parameters.values()}
+ kinds = {p.kind for p in parameters.values()}
arb = {inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD}
if arb.issubset(kinds):
raise TypeError(
@@ -866,7 +853,7 @@ def safe_call(function: Callable[..., T], /, *possible_args: Any, **possible_kwa
for index, parameter, posarg in itertools.zip_longest(
itertools.count(),
- sig.parameters.values(),
+ parameters.values(),
possible_args,
fillvalue=MISSING,
):
@@ -895,24 +882,30 @@ def safe_call(function: Callable[..., T], /, *possible_args: Any, **possible_kwa
def isolate_self(
- sig: inspect.Signature,
+ function: Callable,
+ parameters: Optional[Dict[str, inspect.Parameter]] = None,
) -> Tuple[Tuple[Optional[inspect.Parameter], ...], Dict[str, inspect.Parameter]]:
- """Create parameters without self and the first interaction"""
- parameters = dict(sig.parameters)
- parametersl = list(sig.parameters.values())
+ """Create parameters without self and the first interaction.
+ Optionally accepts a `{str: inspect.Parameter}` dict as an optimization,
+ calls `get_signature_parameters(function)` if not provided.
+ """
+ if parameters is None:
+ parameters = get_signature_parameters(function)
if not parameters:
return (None, None), {}
+ parameters = dict(parameters) # shallow copy
+ parametersl = list(parameters.values())
+
cog_param: Optional[inspect.Parameter] = None
inter_param: Optional[inspect.Parameter] = None
- if parametersl[0].name == "self":
+ if signature_has_self_param(function):
cog_param = parameters.pop(parametersl[0].name)
parametersl.pop(0)
if parametersl:
annot = parametersl[0].annotation
- annot = get_origin(annot) or annot
if issubclass_(annot, ApplicationCommandInteraction) or annot is inspect.Parameter.empty:
inter_param = parameters.pop(parametersl[0].name)
@@ -954,19 +947,15 @@ def classify_autocompleter(autocompleter: AnyAutocompleter) -> None:
def collect_params(
function: Callable,
- sig: Optional[inspect.Signature] = None,
+ parameters: Optional[Dict[str, inspect.Parameter]] = None,
) -> Tuple[Optional[str], Optional[str], List[ParamInfo], Dict[str, Injection]]:
"""Collect all parameters in a function.
- Optionally accepts an `inspect.Signature` object (as an optimization),
- calls `signature(function)` if not provided.
+ Optionally accepts a `{str: inspect.Parameter}` dict as an optimization.
Returns: (`cog parameter`, `interaction parameter`, `param infos`, `injections`)
"""
- if sig is None:
- sig = signature(function)
-
- (cog_param, inter_param), parameters = isolate_self(sig)
+ (cog_param, inter_param), parameters = isolate_self(function, parameters)
doc = disnake.utils.parse_docstring(function)["params"]
@@ -984,9 +973,7 @@ def collect_params(
injections[parameter.name] = default
elif parameter.annotation in Injection._registered:
injections[parameter.name] = Injection._registered[parameter.annotation]
- elif issubclass_(
- get_origin(parameter.annotation) or parameter.annotation, ApplicationCommandInteraction
- ):
+ elif issubclass_(parameter.annotation, ApplicationCommandInteraction):
if inter_param is None:
inter_param = parameter
else:
@@ -1092,10 +1079,10 @@ def expand_params(command: AnySlashCommand) -> List[Option]:
Returns the created options
"""
- sig = signature(command.callback)
- # pass `sig` down to avoid having to call `signature(func)` another time,
+ parameters = get_signature_parameters(command.callback)
+ # pass `parameters` down to avoid having to call `get_signature_parameters(func)` another time,
# which may cause side effects with deferred annotations and warnings
- _, inter_param, params, injections = collect_params(command.callback, sig)
+ _, inter_param, params, injections = collect_params(command.callback, parameters)
if inter_param is None:
raise TypeError(f"Couldn't find an interaction parameter in {command.callback}")
@@ -1120,10 +1107,7 @@ def expand_params(command: AnySlashCommand) -> List[Option]:
if param.autocomplete:
command.autocompleters[param.name] = param.autocomplete
- if issubclass_(
- get_origin(annot := sig.parameters[inter_param].annotation) or annot,
- disnake.GuildCommandInteraction,
- ):
+ if issubclass_(parameters[inter_param].annotation, disnake.GuildCommandInteraction):
command._guild_only = True
return [param.to_option() for param in params]
@@ -1172,8 +1156,8 @@ def Param(
.. versionchanged:: 2.5
Added support for localizations.
- choices: Union[List[:class:`.OptionChoice`], List[Union[:class:`str`, :class:`int`]], Dict[:class:`str`, Union[:class:`str`, :class:`int`]]]
- A list of choices for this option.
+ choices: Union[Sequence[:class:`.OptionChoice`], Sequence[Union[:class:`str`, :class:`int`, :class:`float`]], Mapping[:class:`str`, Union[:class:`str`, :class:`int`, :class:`float`]]]
+ The pre-defined choices for this slash command option.
converter: Callable[[:class:`.ApplicationCommandInteraction`, Any], Any]
A function that will convert the original input to a desired format.
Kwarg aliases: ``conv``.
@@ -1353,7 +1337,7 @@ def option_enum(
choices = choices or kwargs
first, *_ = choices.values()
- return Enum("", choices, type=type(first))
+ return Enum("", choices, type=type(first)) # type: ignore
class ConverterMethod(classmethod):
@@ -1405,12 +1389,11 @@ def register_injection(
:class:`Injection`
The injection being registered.
"""
- sig = signature(function)
- tp = sig.return_annotation
+ tp = get_signature_return(function)
if tp is inspect.Parameter.empty:
raise TypeError("Injection must have a return annotation")
if tp in ParamInfo.TYPES:
raise TypeError("Injection cannot overwrite builtin types")
- return Injection.register(function, sig.return_annotation, autocompleters=autocompleters)
+ return Injection.register(function, tp, autocompleters=autocompleters)
diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py
index 1b318a21d0..a23cf86bd3 100644
--- a/disnake/ext/commands/slash_core.py
+++ b/disnake/ext/commands/slash_core.py
@@ -332,10 +332,12 @@ def parents(
@property
def description(self) -> str:
+ """:class:`str`: The slash sub command's description. Shorthand for :attr:`self.body.description <.Option.description>`."""
return self.body.description
@property
def body(self) -> Option:
+ """:class:`.Option`: The API representation for this slash sub command. Shorthand for :attr:`.SubCommand.option`"""
return self.option
async def _call_autocompleter(
@@ -508,10 +510,12 @@ def _ensure_assignment_on_copy(self, other: SlashCommandT) -> SlashCommandT:
@property
def description(self) -> str:
+ """:class:`str`: The slash command's description. Shorthand for :attr:`self.body.description <.SlashCommand.description>`."""
return self.body.description
@property
def options(self) -> List[Option]:
+ """List[:class:`.Option`]: The list of options the slash command has. Shorthand for :attr:`self.body.options <.SlashCommand.options>`."""
return self.body.options
def sub_command(
@@ -666,7 +670,7 @@ async def _call_relevant_autocompleter(self, inter: ApplicationCommandInteractio
group = self.children.get(chain[0])
if not isinstance(group, SubCommandGroup):
raise AssertionError("the first subcommand is not a SubCommandGroup instance")
- subcmd = group.children.get(chain[1]) if group is not None else None
+ subcmd = group.children.get(chain[1])
else:
raise ValueError("Command chain is too long")
@@ -695,7 +699,7 @@ async def invoke_children(self, inter: ApplicationCommandInteraction) -> None:
group = self.children.get(chain[0])
if not isinstance(group, SubCommandGroup):
raise AssertionError("the first subcommand is not a SubCommandGroup instance")
- subcmd = group.children.get(chain[1]) if group is not None else None
+ subcmd = group.children.get(chain[1])
else:
raise ValueError("Command chain is too long")
diff --git a/disnake/ext/tasks/__init__.py b/disnake/ext/tasks/__init__.py
index 1c23e0e912..6532c3d088 100644
--- a/disnake/ext/tasks/__init__.py
+++ b/disnake/ext/tasks/__init__.py
@@ -708,7 +708,7 @@ class Object(Protocol[T_co, P]):
def __new__(cls) -> T_co:
...
- def __init__(*args: P.args, **kwargs: P.kwargs) -> None:
+ def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None:
...
@@ -734,7 +734,7 @@ def loop(
def loop(
- cls: Type[Object[L_co, Concatenate[LF, P]]] = Loop[LF],
+ cls: Type[Object[L_co, Concatenate[LF, P]]] = Loop[Any],
**kwargs: Any,
) -> Callable[[LF], L_co]:
"""A decorator that schedules a task in the background for you with
diff --git a/disnake/flags.py b/disnake/flags.py
index 66b9b4b369..63fc6bf2c6 100644
--- a/disnake/flags.py
+++ b/disnake/flags.py
@@ -329,7 +329,7 @@ class SystemChannelFlags(BaseFlags, inverted=True):
to enable or disable.
Arguments are applied in order, similar to :class:`Permissions`.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -491,7 +491,7 @@ class MessageFlags(BaseFlags):
See :class:`SystemChannelFlags`.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -681,7 +681,7 @@ def is_voice_message(self):
class PublicUserFlags(BaseFlags):
"""Wraps up the Discord User Public flags.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -930,7 +930,7 @@ class Intents(BaseFlags):
.. versionadded:: 1.5
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -1617,7 +1617,7 @@ class MemberCacheFlags(BaseFlags):
.. versionadded:: 1.5
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -1793,7 +1793,7 @@ def _voice_only(self):
class ApplicationFlags(BaseFlags):
"""Wraps up the Discord Application flags.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -1968,7 +1968,7 @@ def application_command_badge(self):
class ChannelFlags(BaseFlags):
"""Wraps up the Discord Channel flags.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -2081,7 +2081,7 @@ def require_tag(self):
class AutoModKeywordPresets(ListBaseFlags):
"""Wraps up the pre-defined auto moderation keyword lists, provided by Discord.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -2194,7 +2194,7 @@ def slurs(self):
class MemberFlags(BaseFlags):
"""Wraps up Discord Member flags.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -2296,7 +2296,7 @@ def started_onboarding(self):
class RoleFlags(BaseFlags):
"""Wraps up Discord Role flags.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -2376,7 +2376,7 @@ def in_prompt(self):
class AttachmentFlags(BaseFlags):
"""Wraps up Discord Attachment flags.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/gateway.py b/disnake/gateway.py
index 2081493509..cd0cb6d44a 100644
--- a/disnake/gateway.py
+++ b/disnake/gateway.py
@@ -274,7 +274,7 @@ async def close(self, *, code: int = 4000, message: bytes = b"") -> bool:
class HeartbeatWebSocket(Protocol):
- HEARTBEAT: Final[Literal[1, 3]] # type: ignore
+ HEARTBEAT: Final[Literal[1, 3]]
thread_id: int
loop: asyncio.AbstractEventLoop
diff --git a/disnake/guild.py b/disnake/guild.py
index 888c7518d4..449f303c27 100644
--- a/disnake/guild.py
+++ b/disnake/guild.py
@@ -130,7 +130,7 @@ class Guild(Hashable):
This is referred to as a "server" in the official Discord UI.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -2387,7 +2387,7 @@ async def create_scheduled_event(
Creates a :class:`GuildScheduledEvent`.
- You must have :attr:`.Permissions.manage_events` permission to do this.
+ You must have :attr:`~Permissions.manage_events` permission to do this.
Based on the channel/entity type, there are different restrictions regarding
other parameter values, as shown in this table:
@@ -3136,10 +3136,6 @@ async def integrations(self) -> List[Integration]:
def convert(d):
factory, _ = _integration_factory(d["type"])
- if factory is None:
- raise InvalidData(
- "Unknown integration type {type!r} for integration ID {id}".format_map(d)
- )
return factory(guild=self, data=d)
return [convert(d) for d in data]
@@ -3278,7 +3274,7 @@ async def delete_sticker(self, sticker: Snowflake, *, reason: Optional[str] = No
Raises
------
Forbidden
- You are not allowed to delete stickers.
+ You are not allowed to delete this sticker.
HTTPException
An error occurred deleting the sticker.
"""
@@ -3433,7 +3429,7 @@ async def delete_emoji(self, emoji: Snowflake, *, reason: Optional[str] = None)
Raises
------
Forbidden
- You are not allowed to delete emojis.
+ You are not allowed to delete this emoji.
HTTPException
An error occurred deleting the emoji.
"""
diff --git a/disnake/guild_scheduled_event.py b/disnake/guild_scheduled_event.py
index a9739b217a..1b01be136c 100644
--- a/disnake/guild_scheduled_event.py
+++ b/disnake/guild_scheduled_event.py
@@ -77,7 +77,7 @@ class GuildScheduledEvent(Hashable):
.. versionadded:: 2.3
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -253,7 +253,7 @@ async def delete(self) -> None:
Deletes the guild scheduled event.
- You must have :attr:`.Permissions.manage_events` permission to do this.
+ You must have :attr:`~Permissions.manage_events` permission to do this.
Raises
------
@@ -382,7 +382,7 @@ async def edit(
Edits the guild scheduled event.
- You must have :attr:`.Permissions.manage_events` permission to do this.
+ You must have :attr:`~Permissions.manage_events` permission to do this.
.. versionchanged:: 2.6
Updates must follow requirements of :func:`Guild.create_scheduled_event`
@@ -536,7 +536,7 @@ async def start(self, *, reason: Optional[str] = None) -> GuildScheduledEvent:
Changes the event status to :attr:`~GuildScheduledEventStatus.active`.
- You must have :attr:`.Permissions.manage_events` permission to do this.
+ You must have :attr:`~Permissions.manage_events` permission to do this.
.. versionadded:: 2.7
@@ -570,7 +570,7 @@ async def end(self, *, reason: Optional[str] = None) -> GuildScheduledEvent:
Changes the event status to :attr:`~GuildScheduledEventStatus.completed`.
- You must have :attr:`.Permissions.manage_events` permission to do this.
+ You must have :attr:`~Permissions.manage_events` permission to do this.
.. versionadded:: 2.7
@@ -604,7 +604,7 @@ async def cancel(self, *, reason: Optional[str] = None) -> GuildScheduledEvent:
Changes the event status to :attr:`~GuildScheduledEventStatus.cancelled`.
- You must have :attr:`.Permissions.manage_events` permission to do this.
+ You must have :attr:`~Permissions.manage_events` permission to do this.
.. versionadded:: 2.7
diff --git a/disnake/http.py b/disnake/http.py
index f8c4b44694..06b3801861 100644
--- a/disnake/http.py
+++ b/disnake/http.py
@@ -248,19 +248,18 @@ def recreate(self) -> None:
)
async def ws_connect(self, url: str, *, compress: int = 0) -> aiohttp.ClientWebSocketResponse:
- kwargs = {
- "proxy_auth": self.proxy_auth,
- "proxy": self.proxy,
- "max_msg_size": 0,
- "timeout": 30.0,
- "autoclose": False,
- "headers": {
+ return await self.__session.ws_connect(
+ url,
+ proxy_auth=self.proxy_auth,
+ proxy=self.proxy,
+ max_msg_size=0,
+ timeout=30.0,
+ autoclose=False,
+ headers={
"User-Agent": self.user_agent,
},
- "compress": compress,
- }
-
- return await self.__session.ws_connect(url, **kwargs)
+ compress=compress,
+ )
async def request(
self,
@@ -276,9 +275,7 @@ async def request(
lock = self._locks.get(bucket)
if lock is None:
- lock = asyncio.Lock()
- if bucket is not None:
- self._locks[bucket] = lock
+ self._locks[bucket] = lock = asyncio.Lock()
# header creation
headers: Dict[str, str] = {
diff --git a/disnake/i18n.py b/disnake/i18n.py
index 344787ad5b..9a9ca32560 100644
--- a/disnake/i18n.py
+++ b/disnake/i18n.py
@@ -236,8 +236,9 @@ def _copy(self) -> LocalizationValue:
def data(self) -> Optional[Dict[str, str]]:
"""Optional[Dict[:class:`str`, :class:`str`]]: A dict with a locale -> localization mapping, if available."""
if self._data is MISSING:
+ # This will happen when `_link(store)` hasn't been called yet, which *shouldn't* occur under normal circumstances.
warnings.warn(
- f"value ('{self._key}') was never localized, this is likely a library bug",
+ f"Localization value ('{self._key}') was never linked to bot; this may be a library bug.",
LocalizationWarning,
stacklevel=2,
)
@@ -245,6 +246,10 @@ def data(self) -> Optional[Dict[str, str]]:
return self._data
def __eq__(self, other) -> bool:
+ # if both are pending, compare keys instead
+ if self._data is MISSING and other._data is MISSING:
+ return self._key == other._key
+
d1 = self.data
d2 = other.data
# consider values equal if they're both falsy, or actually equal
@@ -409,7 +414,7 @@ def _load_file(self, path: Path) -> None:
except Exception as e:
raise RuntimeError(f"Unable to load '{path}': {e}") from e
- def _load_dict(self, data: Dict[str, str], locale: str) -> None:
+ def _load_dict(self, data: Dict[str, Optional[str]], locale: str) -> None:
if not isinstance(data, dict) or not all(
o is None or isinstance(o, str) for o in data.values()
):
diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py
index bdcbe3cae2..f0058d2cc8 100644
--- a/disnake/interactions/base.py
+++ b/disnake/interactions/base.py
@@ -1260,8 +1260,8 @@ async def autocomplete(self, *, choices: Choices) -> None:
Parameters
----------
- choices: Union[List[:class:`OptionChoice`], List[Union[:class:`str`, :class:`int`]], Dict[:class:`str`, Union[:class:`str`, :class:`int`]]]
- The list of choices to suggest.
+ choices: Union[Sequence[:class:`OptionChoice`], Sequence[Union[:class:`str`, :class:`int`, :class:`float`]], Mapping[:class:`str`, Union[:class:`str`, :class:`int`, :class:`float`]]]
+ The choices to suggest.
Raises
------
@@ -1277,6 +1277,9 @@ async def autocomplete(self, *, choices: Choices) -> None:
if isinstance(choices, Mapping):
choices_data = [{"name": n, "value": v} for n, v in choices.items()]
else:
+ if isinstance(choices, str): # str matches `Sequence[str]`, but isn't meant to be used
+ raise TypeError("choices argument should be a list/sequence or dict, not str")
+
choices_data = []
value: ApplicationCommandOptionChoicePayload
i18n = self._parent.client.i18n
@@ -1855,7 +1858,7 @@ def __init__(
guild
and guild.get_channel_or_thread(channel_id)
or factory(
- guild=guild_fallback, # type: ignore
+ guild=guild_fallback,
state=state,
data=channel, # type: ignore
)
diff --git a/disnake/invite.py b/disnake/invite.py
index 2d95ea6d8a..a936c832b1 100644
--- a/disnake/invite.py
+++ b/disnake/invite.py
@@ -48,7 +48,7 @@ class PartialInviteChannel:
guild the :class:`Invite` resolves to.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -137,7 +137,7 @@ class PartialInviteGuild:
This model will be given when the user is not part of the
guild the :class:`Invite` resolves to.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -256,7 +256,7 @@ class Invite(Hashable):
Depending on the way this object was created, some of the attributes can
have a value of ``None`` (see table below).
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/iterators.py b/disnake/iterators.py
index ea8347effd..f7d694598a 100644
--- a/disnake/iterators.py
+++ b/disnake/iterators.py
@@ -106,7 +106,7 @@ def chunk(self, max_size: int) -> _ChunkedAsyncIterator[T]:
def map(self, func: _Func[T, OT]) -> _MappedAsyncIterator[OT]:
return _MappedAsyncIterator(self, func)
- def filter(self, predicate: _Func[T, bool]) -> _FilteredAsyncIterator[T]:
+ def filter(self, predicate: Optional[_Func[T, bool]]) -> _FilteredAsyncIterator[T]:
return _FilteredAsyncIterator(self, predicate)
async def flatten(self) -> List[T]:
@@ -152,11 +152,11 @@ async def next(self) -> OT:
class _FilteredAsyncIterator(_AsyncIterator[T]):
- def __init__(self, iterator: _AsyncIterator[T], predicate: _Func[T, bool]) -> None:
+ def __init__(self, iterator: _AsyncIterator[T], predicate: Optional[_Func[T, bool]]) -> None:
self.iterator = iterator
if predicate is None:
- predicate = lambda x: bool(x)
+ predicate = bool # similar to the `filter` builtin, a `None` filter drops falsy items
self.predicate: _Func[T, bool] = predicate
@@ -626,8 +626,8 @@ async def _fill(self) -> None:
}
for element in entries:
- # TODO: remove this if statement later
- if element["action_type"] is None:
+ # https://github.com/discord/discord-api-docs/issues/5055#issuecomment-1266363766
+ if element["action_type"] is None: # pyright: ignore[reportUnnecessaryComparison]
continue
await self.entries.put(
diff --git a/disnake/member.py b/disnake/member.py
index 25886079eb..fb1a98e6c8 100644
--- a/disnake/member.py
+++ b/disnake/member.py
@@ -212,7 +212,7 @@ class Member(disnake.abc.Messageable, _UserTag):
This implements a lot of the functionality of :class:`User`.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/message.py b/disnake/message.py
index 21f59e269e..e3967e1160 100644
--- a/disnake/message.py
+++ b/disnake/message.py
@@ -220,7 +220,7 @@ async def _edit_handler(
class Attachment(Hashable):
"""Represents an attachment from Discord.
- .. container:: operations
+ .. collapse:: operations
.. describe:: str(x)
@@ -658,13 +658,14 @@ def __repr__(self) -> str:
return f""
def to_dict(self) -> MessageReferencePayload:
- result: MessageReferencePayload = {"channel_id": self.channel_id}
+ result: MessageReferencePayload = {
+ "channel_id": self.channel_id,
+ "fail_if_not_exists": self.fail_if_not_exists,
+ }
if self.message_id is not None:
result["message_id"] = self.message_id
if self.guild_id is not None:
result["guild_id"] = self.guild_id
- if self.fail_if_not_exists is not None:
- result["fail_if_not_exists"] = self.fail_if_not_exists
return result
to_message_reference_dict = to_dict
@@ -766,7 +767,7 @@ def flatten_handlers(cls):
class Message(Hashable):
"""Represents a message from Discord.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -2177,7 +2178,7 @@ class PartialMessage(Hashable):
.. versionadded:: 1.6
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/object.py b/disnake/object.py
index 9af6a758b7..cd3048b6b1 100644
--- a/disnake/object.py
+++ b/disnake/object.py
@@ -29,7 +29,7 @@ class Object(Hashable):
receive this class rather than the actual data class. These cases are
extremely rare.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/opus.py b/disnake/opus.py
index 596306dd6d..86a3ccbb70 100644
--- a/disnake/opus.py
+++ b/disnake/opus.py
@@ -234,12 +234,12 @@ def _load_default() -> bool:
_bitness = struct.calcsize("P") * 8
_target = "x64" if _bitness > 32 else "x86"
_filename = os.path.join(_basedir, "bin", f"libopus-0.{_target}.dll")
- _lib = libopus_loader(_filename) # noqa: PLW0603
+ _lib = libopus_loader(_filename)
else:
path = ctypes.util.find_library("opus")
if not path:
raise AssertionError("could not find the opus library")
- _lib = libopus_loader(path) # noqa: PLW0603
+ _lib = libopus_loader(path)
except Exception:
_lib = MISSING
diff --git a/disnake/partial_emoji.py b/disnake/partial_emoji.py
index ab124d28e1..92656bb314 100644
--- a/disnake/partial_emoji.py
+++ b/disnake/partial_emoji.py
@@ -38,7 +38,7 @@ class PartialEmoji(_EmojiTag, AssetMixin):
- "Raw" data events such as :func:`on_raw_reaction_add`
- Custom emoji that the bot cannot see from e.g. :attr:`Message.reactions`
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/permissions.py b/disnake/permissions.py
index 8046f14d4a..edad50e84d 100644
--- a/disnake/permissions.py
+++ b/disnake/permissions.py
@@ -76,7 +76,7 @@ class Permissions(BaseFlags):
You can now use keyword arguments to initialize :class:`Permissions`
similar to :meth:`update`.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -164,7 +164,9 @@ def __init__(
ban_members: bool = ...,
change_nickname: bool = ...,
connect: bool = ...,
+ create_events: bool = ...,
create_forum_threads: bool = ...,
+ create_guild_expressions: bool = ...,
create_instant_invite: bool = ...,
create_private_threads: bool = ...,
create_public_threads: bool = ...,
@@ -291,6 +293,7 @@ def all_channel(cls) -> Self:
``True`` and the guild-specific ones set to ``False``. The guild-specific
permissions are currently:
+ - :attr:`create_guild_expressions`
- :attr:`manage_guild_expressions`
- :attr:`view_audit_log`
- :attr:`view_guild_insights`
@@ -316,12 +319,16 @@ def all_channel(cls) -> Self:
.. versionchanged:: 2.9
Added :attr:`use_soundboard` and :attr:`send_voice_messages` permissions.
+
+ .. versionchanged:: 2.10
+ Added :attr:`create_events` permission.
"""
instance = cls.all()
instance.update(
administrator=False,
ban_members=False,
change_nickname=False,
+ create_guild_expressions=False,
kick_members=False,
manage_guild=False,
manage_guild_expressions=False,
@@ -347,11 +354,15 @@ def general(cls) -> Self:
.. versionchanged:: 2.9
Added :attr:`view_creator_monetization_analytics` permission.
+
+ .. versionchanged:: 2.10
+ Added :attr:`create_guild_expressions` permission.
"""
return cls(
view_channel=True,
manage_channels=True,
manage_roles=True,
+ create_guild_expressions=True,
manage_guild_expressions=True,
view_audit_log=True,
view_guild_insights=True,
@@ -475,8 +486,12 @@ def events(cls) -> Self:
"Events" permissions from the official Discord UI set to ``True``.
.. versionadded:: 2.4
+
+ .. versionchanged:: 2.10
+ Added :attr:`create_events` permission.
"""
return cls(
+ create_events=True,
manage_events=True,
)
@@ -532,7 +547,9 @@ def update(
ban_members: bool = ...,
change_nickname: bool = ...,
connect: bool = ...,
+ create_events: bool = ...,
create_forum_threads: bool = ...,
+ create_guild_expressions: bool = ...,
create_instant_invite: bool = ...,
create_private_threads: bool = ...,
create_public_threads: bool = ...,
@@ -830,8 +847,10 @@ def manage_webhooks(self) -> int:
@flag_value
def manage_guild_expressions(self) -> int:
- """:class:`bool`: Returns ``True`` if a user can create, edit, or delete
- emojis, stickers, and soundboard sounds.
+ """:class:`bool`: Returns ``True`` if a user can edit or delete
+ emojis, stickers, and soundboard sounds created by all users.
+
+ See also :attr:`create_guild_expressions`.
.. versionadded:: 2.9
"""
@@ -879,7 +898,10 @@ def request_to_speak(self) -> int:
@flag_value
def manage_events(self) -> int:
- """:class:`bool`: Returns ``True`` if a user can manage guild events.
+ """:class:`bool`: Returns ``True`` if a user can edit or delete guild scheduled events
+ created by all users.
+
+ See also :attr:`create_events`.
.. versionadded:: 2.0
"""
@@ -978,6 +1000,28 @@ def use_soundboard(self) -> int:
"""
return 1 << 42
+ @flag_value
+ def create_guild_expressions(self) -> int:
+ """:class:`bool`: Returns ``True`` if a user can create emojis, stickers,
+ and soundboard sounds, as well as edit and delete the ones they created.
+
+ See also :attr:`manage_guild_expressions`.
+
+ .. versionadded:: 2.10
+ """
+ return 1 << 43
+
+ @flag_value
+ def create_events(self) -> int:
+ """:class:`bool`: Returns ``True`` if a user can create guild scheduled events,
+ as well as edit and delete the ones they created.
+
+ See also :attr:`manage_events`.
+
+ .. versionadded:: 2.10
+ """
+ return 1 << 44
+
@flag_value
def use_external_sounds(self) -> int:
""":class:`bool`: Returns ``True`` if a user can use custom soundboard sounds from other guilds.
@@ -1036,7 +1080,7 @@ class PermissionOverwrite:
The values supported by this are the same as :class:`Permissions`
with the added possibility of it being set to ``None``.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -1066,7 +1110,9 @@ class PermissionOverwrite:
ban_members: Optional[bool]
change_nickname: Optional[bool]
connect: Optional[bool]
+ create_events: Optional[bool]
create_forum_threads: Optional[bool]
+ create_guild_expressions: Optional[bool]
create_instant_invite: Optional[bool]
create_private_threads: Optional[bool]
create_public_threads: Optional[bool]
@@ -1130,7 +1176,9 @@ def __init__(
ban_members: Optional[bool] = ...,
change_nickname: Optional[bool] = ...,
connect: Optional[bool] = ...,
+ create_events: Optional[bool] = ...,
create_forum_threads: Optional[bool] = ...,
+ create_guild_expressions: Optional[bool] = ...,
create_instant_invite: Optional[bool] = ...,
create_private_threads: Optional[bool] = ...,
create_public_threads: Optional[bool] = ...,
@@ -1261,7 +1309,9 @@ def update(
ban_members: Optional[bool] = ...,
change_nickname: Optional[bool] = ...,
connect: Optional[bool] = ...,
+ create_events: Optional[bool] = ...,
create_forum_threads: Optional[bool] = ...,
+ create_guild_expressions: Optional[bool] = ...,
create_instant_invite: Optional[bool] = ...,
create_private_threads: Optional[bool] = ...,
create_public_threads: Optional[bool] = ...,
diff --git a/disnake/reaction.py b/disnake/reaction.py
index 5a3c784627..0720759f6a 100644
--- a/disnake/reaction.py
+++ b/disnake/reaction.py
@@ -22,7 +22,7 @@ class Reaction:
Depending on the way this object was created, some of the attributes can
have a value of ``None``.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/role.py b/disnake/role.py
index addd6b7551..89fa55804f 100644
--- a/disnake/role.py
+++ b/disnake/role.py
@@ -140,7 +140,7 @@ def __repr__(self) -> str:
class Role(Hashable):
"""Represents a Discord role in a :class:`Guild`.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/shard.py b/disnake/shard.py
index 102c66e4ae..a82ae13efd 100644
--- a/disnake/shard.py
+++ b/disnake/shard.py
@@ -589,7 +589,8 @@ async def change_presence(
activities = () if activity is None else (activity,)
for guild in guilds:
me = guild.me
- if me is None:
+ if me is None: # pyright: ignore[reportUnnecessaryComparison]
+ # may happen if guild is unavailable
continue
# Member.activities is typehinted as Tuple[ActivityType, ...], we may be setting it as Tuple[BaseActivity, ...]
diff --git a/disnake/stage_instance.py b/disnake/stage_instance.py
index 08f50dc3e1..deff882916 100644
--- a/disnake/stage_instance.py
+++ b/disnake/stage_instance.py
@@ -24,7 +24,7 @@ class StageInstance(Hashable):
.. versionadded:: 2.0
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/state.py b/disnake/state.py
index ca915aa33f..714a92759b 100644
--- a/disnake/state.py
+++ b/disnake/state.py
@@ -25,7 +25,6 @@
Tuple,
TypeVar,
Union,
- cast,
overload,
)
@@ -600,7 +599,6 @@ def _get_guild_channel(
if channel is None:
if "author" in data:
# MessagePayload
- data = cast("MessagePayload", data)
user_id = int(data["author"]["id"])
else:
# TypingStartEvent
@@ -637,8 +635,6 @@ async def query_members(
):
guild_id = guild.id
ws = self._get_websocket(guild_id)
- if ws is None:
- raise RuntimeError("Somehow do not have a websocket for this guild_id")
request = ChunkRequest(guild.id, self.loop, self._get_guild, cache=cache)
self._chunk_requests[request.nonce] = request
@@ -1796,6 +1792,8 @@ def parse_voice_server_update(self, data: gateway.VoiceServerUpdateEvent) -> Non
logging_coroutine(coro, info="Voice Protocol voice server update handler")
)
+ # FIXME: this should be refactored. The `GroupChannel` path will never be hit,
+ # `raw.timestamp` exists so no need to parse it twice, and `.get_user` should be used before falling back
def parse_typing_start(self, data: gateway.TypingStartEvent) -> None:
channel, guild = self._get_guild_channel(data)
raw = RawTypingEvent(data)
@@ -1810,7 +1808,7 @@ def parse_typing_start(self, data: gateway.TypingStartEvent) -> None:
self.dispatch("raw_typing", raw)
- if channel is not None:
+ if channel is not None: # pyright: ignore[reportUnnecessaryComparison]
member = None
if raw.member is not None:
member = raw.member
diff --git a/disnake/sticker.py b/disnake/sticker.py
index 01ce53b9d3..be7479cf2b 100644
--- a/disnake/sticker.py
+++ b/disnake/sticker.py
@@ -44,7 +44,7 @@ class StickerPack(Hashable):
.. versionchanged:: 2.8
:attr:`cover_sticker_id`, :attr:`cover_sticker` and :attr:`banner` are now optional.
- .. container:: operations
+ .. collapse:: operations
.. describe:: str(x)
@@ -163,7 +163,7 @@ class StickerItem(_StickerTag):
.. versionadded:: 2.0
- .. container:: operations
+ .. collapse:: operations
.. describe:: str(x)
@@ -226,7 +226,7 @@ class Sticker(_StickerTag):
.. versionadded:: 1.6
- .. container:: operations
+ .. collapse:: operations
.. describe:: str(x)
@@ -283,7 +283,7 @@ class StandardSticker(Sticker):
.. versionadded:: 2.0
- .. container:: operations
+ .. collapse:: operations
.. describe:: str(x)
@@ -362,7 +362,7 @@ class GuildSticker(Sticker):
.. versionadded:: 2.0
- .. container:: operations
+ .. collapse:: operations
.. describe:: str(x)
@@ -450,7 +450,7 @@ async def edit(
Raises
------
Forbidden
- You are not allowed to edit stickers.
+ You are not allowed to edit this sticker.
HTTPException
An error occurred editing the sticker.
@@ -498,7 +498,7 @@ async def delete(self, *, reason: Optional[str] = None) -> None:
Raises
------
Forbidden
- You are not allowed to delete stickers.
+ You are not allowed to delete this sticker.
HTTPException
An error occurred deleting the sticker.
"""
diff --git a/disnake/team.py b/disnake/team.py
index 1034904cd9..a1f126304e 100644
--- a/disnake/team.py
+++ b/disnake/team.py
@@ -7,7 +7,7 @@
from . import utils
from .asset import Asset
-from .enums import TeamMembershipState, try_enum
+from .enums import TeamMemberRole, TeamMembershipState, try_enum
from .user import BaseUser
if TYPE_CHECKING:
@@ -21,7 +21,8 @@
class Team:
- """Represents an application team for a bot provided by Discord.
+ """Represents an application team.
+ Teams are groups of users who share access to an application's configuration.
Attributes
----------
@@ -30,7 +31,7 @@ class Team:
name: :class:`str`
The team name.
owner_id: :class:`int`
- The team's owner ID.
+ The team owner's ID.
members: List[:class:`TeamMember`]
A list of the members in the team.
@@ -44,7 +45,7 @@ def __init__(self, state: ConnectionState, data: TeamPayload) -> None:
self.id: int = int(data["id"])
self.name: str = data["name"]
- self._icon: Optional[str] = data["icon"]
+ self._icon: Optional[str] = data.get("icon")
self.owner_id: Optional[int] = utils._get_as_snowflake(data, "owner_user_id")
self.members: List[TeamMember] = [
TeamMember(self, self._state, member) for member in data["members"]
@@ -77,7 +78,7 @@ def owner(self) -> Optional[TeamMember]:
class TeamMember(BaseUser):
"""Represents a team member in a team.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -113,29 +114,27 @@ class TeamMember(BaseUser):
See the `help article `__ for details.
global_name: Optional[:class:`str`]
- The team members's global display name, if set.
+ The team member's global display name, if set.
This takes precedence over :attr:`.name` when shown.
.. versionadded:: 2.9
- avatar: Optional[:class:`str`]
- The avatar hash the team member has. Could be None.
- bot: :class:`bool`
- Specifies if the user is a bot account.
team: :class:`Team`
The team that the member is from.
membership_state: :class:`TeamMembershipState`
- The membership state of the member (e.g. invited or accepted)
+ The membership state of the member (e.g. invited or accepted).
+ role: :class:`TeamMemberRole`
+ The role of the team member in the team.
"""
- __slots__ = ("team", "membership_state", "permissions")
+ __slots__ = ("team", "membership_state", "role")
def __init__(self, team: Team, state: ConnectionState, data: TeamMemberPayload) -> None:
self.team: Team = team
self.membership_state: TeamMembershipState = try_enum(
TeamMembershipState, data["membership_state"]
)
- self.permissions: List[str] = data["permissions"]
+ self.role: TeamMemberRole = try_enum(TeamMemberRole, data.get("role"))
super().__init__(state=state, data=data["user"])
def __repr__(self) -> str:
diff --git a/disnake/threads.py b/disnake/threads.py
index 2457c5a879..fb0b2add92 100644
--- a/disnake/threads.py
+++ b/disnake/threads.py
@@ -12,6 +12,7 @@
from .flags import ChannelFlags
from .mixins import Hashable
from .partial_emoji import PartialEmoji, _EmojiTag
+from .permissions import Permissions
from .utils import MISSING, _get_as_snowflake, _unique, parse_time, snowflake_time
__all__ = (
@@ -31,7 +32,6 @@
from .guild import Guild
from .member import Member
from .message import Message, PartialMessage
- from .permissions import Permissions
from .role import Role
from .state import ConnectionState
from .types.snowflake import SnowflakeList
@@ -54,7 +54,7 @@
class Thread(Messageable, Hashable):
"""Represents a Discord thread.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -1018,7 +1018,7 @@ def _pop_member(self, member_id: int) -> Optional[ThreadMember]:
class ThreadMember(Hashable):
"""Represents a Discord thread member.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -1092,7 +1092,7 @@ def thread(self) -> Thread:
class ForumTag(Hashable):
"""Represents a tag for threads in forum channels.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/types/audit_log.py b/disnake/types/audit_log.py
index d3b3a5484f..f9640b3ad9 100644
--- a/disnake/types/audit_log.py
+++ b/disnake/types/audit_log.py
@@ -103,8 +103,8 @@ class _AuditLogChange_Str(TypedDict):
"permissions",
"tags",
]
- new_value: str
- old_value: str
+ new_value: NotRequired[str]
+ old_value: NotRequired[str]
class _AuditLogChange_AssetHash(TypedDict):
@@ -116,8 +116,8 @@ class _AuditLogChange_AssetHash(TypedDict):
"avatar_hash",
"asset",
]
- new_value: str
- old_value: str
+ new_value: NotRequired[str]
+ old_value: NotRequired[str]
class _AuditLogChange_Snowflake(TypedDict):
@@ -134,8 +134,8 @@ class _AuditLogChange_Snowflake(TypedDict):
"inviter_id",
"guild_id",
]
- new_value: Snowflake
- old_value: Snowflake
+ new_value: NotRequired[Snowflake]
+ old_value: NotRequired[Snowflake]
class _AuditLogChange_Bool(TypedDict):
@@ -157,8 +157,8 @@ class _AuditLogChange_Bool(TypedDict):
"premium_progress_bar_enabled",
"enabled",
]
- new_value: bool
- old_value: bool
+ new_value: NotRequired[bool]
+ old_value: NotRequired[bool]
class _AuditLogChange_Int(TypedDict):
@@ -175,104 +175,104 @@ class _AuditLogChange_Int(TypedDict):
"auto_archive_duration",
"default_auto_archive_duration",
]
- new_value: int
- old_value: int
+ new_value: NotRequired[int]
+ old_value: NotRequired[int]
class _AuditLogChange_ListSnowflake(TypedDict):
key: Literal["exempt_roles", "exempt_channels"]
- new_value: List[Snowflake]
- old_value: List[Snowflake]
+ new_value: NotRequired[List[Snowflake]]
+ old_value: NotRequired[List[Snowflake]]
class _AuditLogChange_ListRole(TypedDict):
key: Literal["$add", "$remove"]
- new_value: List[Role]
- old_value: List[Role]
+ new_value: NotRequired[List[Role]]
+ old_value: NotRequired[List[Role]]
class _AuditLogChange_MFALevel(TypedDict):
key: Literal["mfa_level"]
- new_value: MFALevel
- old_value: MFALevel
+ new_value: NotRequired[MFALevel]
+ old_value: NotRequired[MFALevel]
class _AuditLogChange_VerificationLevel(TypedDict):
key: Literal["verification_level"]
- new_value: VerificationLevel
- old_value: VerificationLevel
+ new_value: NotRequired[VerificationLevel]
+ old_value: NotRequired[VerificationLevel]
class _AuditLogChange_ExplicitContentFilter(TypedDict):
key: Literal["explicit_content_filter"]
- new_value: ExplicitContentFilterLevel
- old_value: ExplicitContentFilterLevel
+ new_value: NotRequired[ExplicitContentFilterLevel]
+ old_value: NotRequired[ExplicitContentFilterLevel]
class _AuditLogChange_DefaultMessageNotificationLevel(TypedDict):
key: Literal["default_message_notifications"]
- new_value: DefaultMessageNotificationLevel
- old_value: DefaultMessageNotificationLevel
+ new_value: NotRequired[DefaultMessageNotificationLevel]
+ old_value: NotRequired[DefaultMessageNotificationLevel]
class _AuditLogChange_ChannelType(TypedDict):
key: Literal["type"]
- new_value: ChannelType
- old_value: ChannelType
+ new_value: NotRequired[ChannelType]
+ old_value: NotRequired[ChannelType]
class _AuditLogChange_IntegrationExpireBehaviour(TypedDict):
key: Literal["expire_behavior"]
- new_value: IntegrationExpireBehavior
- old_value: IntegrationExpireBehavior
+ new_value: NotRequired[IntegrationExpireBehavior]
+ old_value: NotRequired[IntegrationExpireBehavior]
class _AuditLogChange_VideoQualityMode(TypedDict):
key: Literal["video_quality_mode"]
- new_value: VideoQualityMode
- old_value: VideoQualityMode
+ new_value: NotRequired[VideoQualityMode]
+ old_value: NotRequired[VideoQualityMode]
class _AuditLogChange_Overwrites(TypedDict):
key: Literal["permission_overwrites"]
- new_value: List[PermissionOverwrite]
- old_value: List[PermissionOverwrite]
+ new_value: NotRequired[List[PermissionOverwrite]]
+ old_value: NotRequired[List[PermissionOverwrite]]
class _AuditLogChange_Datetime(TypedDict):
key: Literal["communication_disabled_until"]
- new_value: datetime.datetime
- old_value: datetime.datetime
+ new_value: NotRequired[datetime.datetime]
+ old_value: NotRequired[datetime.datetime]
class _AuditLogChange_ApplicationCommandPermissions(TypedDict):
key: str
- new_value: ApplicationCommandPermissions
- old_value: ApplicationCommandPermissions
+ new_value: NotRequired[ApplicationCommandPermissions]
+ old_value: NotRequired[ApplicationCommandPermissions]
class _AuditLogChange_AutoModTriggerType(TypedDict):
key: Literal["trigger_type"]
- new_value: AutoModTriggerType
- old_value: AutoModTriggerType
+ new_value: NotRequired[AutoModTriggerType]
+ old_value: NotRequired[AutoModTriggerType]
class _AuditLogChange_AutoModEventType(TypedDict):
key: Literal["event_type"]
- new_value: AutoModEventType
- old_value: AutoModEventType
+ new_value: NotRequired[AutoModEventType]
+ old_value: NotRequired[AutoModEventType]
class _AuditLogChange_AutoModActions(TypedDict):
key: Literal["actions"]
- new_value: List[AutoModAction]
- old_value: List[AutoModAction]
+ new_value: NotRequired[List[AutoModAction]]
+ old_value: NotRequired[List[AutoModAction]]
class _AuditLogChange_AutoModTriggerMetadata(TypedDict):
key: Literal["trigger_metadata"]
- new_value: AutoModTriggerMetadata
- old_value: AutoModTriggerMetadata
+ new_value: NotRequired[AutoModTriggerMetadata]
+ old_value: NotRequired[AutoModTriggerMetadata]
AuditLogChange = Union[
diff --git a/disnake/types/automod.py b/disnake/types/automod.py
index 156952d092..f7ac372e5e 100644
--- a/disnake/types/automod.py
+++ b/disnake/types/automod.py
@@ -8,9 +8,9 @@
from .snowflake import Snowflake, SnowflakeList
-AutoModTriggerType = Literal[1, 2, 3, 4, 5]
+AutoModTriggerType = Literal[1, 3, 4, 5]
AutoModEventType = Literal[1]
-AutoModActionType = Literal[1, 2]
+AutoModActionType = Literal[1, 2, 3]
AutoModPresetType = Literal[1, 2, 3]
diff --git a/disnake/types/team.py b/disnake/types/team.py
index 5662365e03..0829c18b5c 100644
--- a/disnake/types/team.py
+++ b/disnake/types/team.py
@@ -8,13 +8,14 @@
from .user import PartialUser
TeamMembershipState = Literal[1, 2]
+TeamMemberRole = Literal["admin", "developer", "read_only"]
class TeamMember(TypedDict):
- user: PartialUser
membership_state: TeamMembershipState
- permissions: List[str]
team_id: Snowflake
+ user: PartialUser
+ role: TeamMemberRole
class Team(TypedDict):
diff --git a/disnake/types/template.py b/disnake/types/template.py
index ddb2c26cb7..e0008659aa 100644
--- a/disnake/types/template.py
+++ b/disnake/types/template.py
@@ -20,7 +20,7 @@ class Template(TypedDict):
description: Optional[str]
usage_count: int
creator_id: Snowflake
- creator: User
+ creator: Optional[User] # unsure when this can be null, but the spec says so
created_at: str
updated_at: str
source_guild_id: Snowflake
diff --git a/disnake/ui/action_row.py b/disnake/ui/action_row.py
index b8473badb0..21ea01cb74 100644
--- a/disnake/ui/action_row.py
+++ b/disnake/ui/action_row.py
@@ -91,7 +91,7 @@
class ActionRow(Generic[UIComponentT]):
"""Represents a UI action row. Useful for lower level component manipulation.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x[i]
@@ -159,7 +159,8 @@ def __init__(self: ActionRow[ModalUIComponent], *components: ModalUIComponent) -
def __init__(self: ActionRow[StrictUIComponentT], *components: StrictUIComponentT) -> None:
...
- def __init__(self, *components: UIComponentT) -> None:
+ # n.b. this should be `*components: UIComponentT`, but pyright does not like it
+ def __init__(self, *components: Union[MessageUIComponent, ModalUIComponent]) -> None:
self._children: List[UIComponentT] = []
for component in components:
@@ -167,7 +168,7 @@ def __init__(self, *components: UIComponentT) -> None:
raise TypeError(
f"components should be of type WrappedComponent, got {type(component).__name__}."
)
- self.append_item(component)
+ self.append_item(component) # type: ignore
def __repr__(self) -> str:
return f""
diff --git a/disnake/ui/button.py b/disnake/ui/button.py
index 8bb1e60ff2..a961ba29ab 100644
--- a/disnake/ui/button.py
+++ b/disnake/ui/button.py
@@ -275,7 +275,7 @@ def button(
def button(
- cls: Type[Object[B_co, P]] = Button[Any], **kwargs: Any
+ cls: Type[Object[B_co, ...]] = Button[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[B_co]], DecoratedItem[B_co]]:
"""A decorator that attaches a button to a component.
@@ -295,7 +295,7 @@ def button(
----------
cls: Type[:class:`Button`]
The button subclass to create an instance of. If provided, the following parameters
- described below do no apply. Instead, this decorator will accept the same keywords
+ described below do not apply. Instead, this decorator will accept the same keywords
as the passed cls does.
.. versionadded:: 2.6
diff --git a/disnake/ui/item.py b/disnake/ui/item.py
index 971ca8dcb3..464eb4d588 100644
--- a/disnake/ui/item.py
+++ b/disnake/ui/item.py
@@ -184,5 +184,5 @@ class Object(Protocol[T_co, P]):
def __new__(cls) -> T_co:
...
- def __init__(*args: P.args, **kwargs: P.kwargs) -> None:
+ def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None:
...
diff --git a/disnake/ui/modal.py b/disnake/ui/modal.py
index a7a5503a28..adf21ffa9c 100644
--- a/disnake/ui/modal.py
+++ b/disnake/ui/modal.py
@@ -55,7 +55,7 @@ def __init__(
custom_id: str = MISSING,
timeout: float = 600,
) -> None:
- if timeout is None:
+ if timeout is None: # pyright: ignore[reportUnnecessaryComparison]
raise ValueError("Timeout may not be None")
rows = components_to_rows(components)
diff --git a/disnake/ui/select/channel.py b/disnake/ui/select/channel.py
index a455172799..57dd9cfbe9 100644
--- a/disnake/ui/select/channel.py
+++ b/disnake/ui/select/channel.py
@@ -168,9 +168,7 @@ def channel_select(
def channel_select(
- cls: Type[Object[S_co, P]] = ChannelSelect[Any],
- /,
- **kwargs: Any,
+ cls: Type[Object[S_co, ...]] = ChannelSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a channel select menu to a component.
@@ -187,7 +185,7 @@ def channel_select(
----------
cls: Type[:class:`ChannelSelect`]
The select subclass to create an instance of. If provided, the following parameters
- described below do no apply. Instead, this decorator will accept the same keywords
+ described below do not apply. Instead, this decorator will accept the same keywords
as the passed cls does.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py
index c9e5802f78..860903f7f1 100644
--- a/disnake/ui/select/mentionable.py
+++ b/disnake/ui/select/mentionable.py
@@ -144,9 +144,7 @@ def mentionable_select(
def mentionable_select(
- cls: Type[Object[S_co, P]] = MentionableSelect[Any],
- /,
- **kwargs: Any,
+ cls: Type[Object[S_co, ...]] = MentionableSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a mentionable (user/member/role) select menu to a component.
@@ -163,7 +161,7 @@ def mentionable_select(
----------
cls: Type[:class:`MentionableSelect`]
The select subclass to create an instance of. If provided, the following parameters
- described below do no apply. Instead, this decorator will accept the same keywords
+ described below do not apply. Instead, this decorator will accept the same keywords
as the passed cls does.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py
index 4644b9a660..fe2da2f97a 100644
--- a/disnake/ui/select/role.py
+++ b/disnake/ui/select/role.py
@@ -142,9 +142,7 @@ def role_select(
def role_select(
- cls: Type[Object[S_co, P]] = RoleSelect[Any],
- /,
- **kwargs: Any,
+ cls: Type[Object[S_co, ...]] = RoleSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a role select menu to a component.
@@ -161,7 +159,7 @@ def role_select(
----------
cls: Type[:class:`RoleSelect`]
The select subclass to create an instance of. If provided, the following parameters
- described below do no apply. Instead, this decorator will accept the same keywords
+ described below do not apply. Instead, this decorator will accept the same keywords
as the passed cls does.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
diff --git a/disnake/ui/select/string.py b/disnake/ui/select/string.py
index 0a975c2aa8..3eeedc1f22 100644
--- a/disnake/ui/select/string.py
+++ b/disnake/ui/select/string.py
@@ -268,9 +268,7 @@ def string_select(
def string_select(
- cls: Type[Object[S_co, P]] = StringSelect[Any],
- /,
- **kwargs: Any,
+ cls: Type[Object[S_co, ...]] = StringSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a string select menu to a component.
@@ -288,7 +286,7 @@ def string_select(
----------
cls: Type[:class:`StringSelect`]
The select subclass to create an instance of. If provided, the following parameters
- described below do no apply. Instead, this decorator will accept the same keywords
+ described below do not apply. Instead, this decorator will accept the same keywords
as the passed cls does.
.. versionadded:: 2.6
diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py
index 9a995739fc..4868894a83 100644
--- a/disnake/ui/select/user.py
+++ b/disnake/ui/select/user.py
@@ -143,9 +143,7 @@ def user_select(
def user_select(
- cls: Type[Object[S_co, P]] = UserSelect[Any],
- /,
- **kwargs: Any,
+ cls: Type[Object[S_co, ...]] = UserSelect[Any], **kwargs: Any
) -> Callable[[ItemCallbackType[S_co]], DecoratedItem[S_co]]:
"""A decorator that attaches a user select menu to a component.
@@ -162,7 +160,7 @@ def user_select(
----------
cls: Type[:class:`UserSelect`]
The select subclass to create an instance of. If provided, the following parameters
- described below do no apply. Instead, this decorator will accept the same keywords
+ described below do not apply. Instead, this decorator will accept the same keywords
as the passed cls does.
placeholder: Optional[:class:`str`]
The placeholder text that is shown if nothing is selected, if any.
diff --git a/disnake/user.py b/disnake/user.py
index b2b05acb54..4326016100 100644
--- a/disnake/user.py
+++ b/disnake/user.py
@@ -281,7 +281,7 @@ def mentioned_in(self, message: Message) -> bool:
class ClientUser(BaseUser):
"""Represents your Discord user.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -419,7 +419,7 @@ async def edit(
class User(BaseUser, disnake.abc.Messageable):
"""Represents a Discord user.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/utils.py b/disnake/utils.py
index 15fd82ff57..f4c9e5fb9a 100644
--- a/disnake/utils.py
+++ b/disnake/utils.py
@@ -6,11 +6,13 @@
import asyncio
import datetime
import functools
+import inspect
import json
import os
import pkgutil
import re
import sys
+import types
import unicodedata
import warnings
from base64 import b64encode
@@ -142,6 +144,7 @@ class _RequestLike(Protocol):
V = TypeVar("V")
T_co = TypeVar("T_co", covariant=True)
_Iter = Union[Iterator[T], AsyncIterator[T]]
+_BytesLike = Union[bytes, bytearray, memoryview]
class CachedSlotProperty(Generic[T, T_co]):
@@ -497,7 +500,7 @@ def _maybe_cast(value: V, converter: Callable[[V], T], default: T = None) -> Opt
}
-def _get_mime_type_for_image(data: bytes) -> str:
+def _get_mime_type_for_image(data: _BytesLike) -> str:
if data[0:8] == b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A":
return "image/png"
elif data[0:3] == b"\xff\xd8\xff" or data[6:10] in (b"JFIF", b"Exif"):
@@ -510,14 +513,14 @@ def _get_mime_type_for_image(data: bytes) -> str:
raise ValueError("Unsupported image type given")
-def _bytes_to_base64_data(data: bytes) -> str:
+def _bytes_to_base64_data(data: _BytesLike) -> str:
fmt = "data:{mime};base64,{data}"
mime = _get_mime_type_for_image(data)
b64 = b64encode(data).decode("ascii")
return fmt.format(mime=mime, data=b64)
-def _get_extension_for_image(data: bytes) -> Optional[str]:
+def _get_extension_for_image(data: _BytesLike) -> Optional[str]:
try:
mime_type = _get_mime_type_for_image(data)
except ValueError:
@@ -546,7 +549,7 @@ async def _assetbytes_to_base64_data(data: Optional[AssetBytes]) -> Optional[str
if HAS_ORJSON:
def _to_json(obj: Any) -> str:
- return orjson.dumps(obj).decode("utf-8")
+ return orjson.dumps(obj).decode("utf-8") # type: ignore
_from_json = orjson.loads # type: ignore
@@ -579,7 +582,8 @@ async def maybe_coroutine(
return value # type: ignore # typeguard doesn't narrow in the negative case
-async def async_all(gen: Iterable[Union[Awaitable[bool], bool]], *, check=_isawaitable) -> bool:
+async def async_all(gen: Iterable[Union[Awaitable[bool], bool]]) -> bool:
+ check = _isawaitable
for elem in gen:
if check(elem):
elem = await elem
@@ -828,7 +832,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, flags=re.MULTILINE)
def escape_markdown(text: str, *, as_needed: bool = False, ignore_links: bool = True) -> str:
@@ -867,7 +871,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, flags=re.MULTILINE)
else:
text = re.sub(r"\\", r"\\\\", text)
return _MARKDOWN_ESCAPE_REGEX.sub(r"\\\1", text)
@@ -1125,6 +1129,24 @@ def normalise_optional_params(parameters: Iterable[Any]) -> Tuple[Any, ...]:
return tuple(p for p in parameters if p is not none_cls) + (none_cls,)
+def _resolve_typealiastype(
+ tp: Any, globals: Dict[str, Any], locals: Dict[str, Any], cache: Dict[str, Any]
+):
+ # Use __module__ to get the (global) namespace in which the type alias was defined.
+ if mod := sys.modules.get(tp.__module__):
+ mod_globals = mod.__dict__
+ if mod_globals is not globals or mod_globals is not locals:
+ # if the namespace changed (usually when a TypeAliasType was imported from a different module),
+ # drop the cache since names can resolve differently now
+ cache = {}
+ globals = locals = mod_globals
+
+ # Accessing `__value__` automatically evaluates the type alias in the annotation scope.
+ # (recurse to resolve possible forwardrefs, aliases, etc.)
+ return evaluate_annotation(tp.__value__, globals, locals, cache)
+
+
+# FIXME: this should be split up into smaller functions for clarity and easier maintenance
def evaluate_annotation(
tp: Any,
globals: Dict[str, Any],
@@ -1141,29 +1163,40 @@ def evaluate_annotation(
if implicit_str and isinstance(tp, str):
if tp in cache:
return cache[tp]
- evaluated = eval( # noqa: PGH001 # this is how annotations are supposed to be unstringifed
- tp, globals, locals
- )
+
+ # this is how annotations are supposed to be unstringifed
+ evaluated = eval(tp, globals, locals) # noqa: PGH001, S307
+ # recurse to resolve nested args further
+ evaluated = evaluate_annotation(evaluated, globals, locals, cache)
+
cache[tp] = evaluated
- return evaluate_annotation(evaluated, globals, locals, cache)
+ return evaluated
+ # GenericAlias / UnionType
if hasattr(tp, "__args__"):
- implicit_str = True
- is_literal = False
- orig_args = args = tp.__args__
if not hasattr(tp, "__origin__"):
if tp.__class__ is UnionType:
- converted = Union[args] # type: ignore
+ converted = Union[tp.__args__] # type: ignore
return evaluate_annotation(converted, globals, locals, cache)
return tp
- if tp.__origin__ is Union:
+
+ implicit_str = True
+ is_literal = False
+ orig_args = args = tp.__args__
+ orig_origin = origin = tp.__origin__
+
+ # origin can be a TypeAliasType too, resolve it and continue
+ if hasattr(origin, "__value__"):
+ origin = _resolve_typealiastype(origin, globals, locals, cache)
+
+ if origin is Union:
try:
if args.index(type(None)) != len(args) - 1:
args = normalise_optional_params(tp.__args__)
except ValueError:
pass
- if tp.__origin__ is Literal:
+ if origin is Literal:
if not PY_310:
args = flatten_literal_params(tp.__args__)
implicit_str = False
@@ -1179,13 +1212,21 @@ def evaluate_annotation(
):
raise TypeError("Literal arguments must be of type str, int, bool, or NoneType.")
+ if origin != orig_origin:
+ # we can't use `copy_with` in this case, so just skip all of the following logic
+ return origin[evaluated_args]
+
if evaluated_args == orig_args:
return tp
try:
return tp.copy_with(evaluated_args)
except AttributeError:
- return tp.__origin__[evaluated_args]
+ return origin[evaluated_args]
+
+ # TypeAliasType, 3.12+
+ if hasattr(tp, "__value__"):
+ return _resolve_typealiastype(tp, globals, locals, cache)
return tp
@@ -1207,6 +1248,137 @@ def resolve_annotation(
return evaluate_annotation(annotation, globalns, locals, cache)
+def unwrap_function(function: Callable[..., Any]) -> Callable[..., Any]:
+ partial = functools.partial
+ while True:
+ if hasattr(function, "__wrapped__"):
+ function = function.__wrapped__
+ elif isinstance(function, partial):
+ function = function.func
+ else:
+ return function
+
+
+def _get_function_globals(function: Callable[..., Any]) -> Dict[str, Any]:
+ unwrap = unwrap_function(function)
+ try:
+ return unwrap.__globals__
+ except AttributeError:
+ return {}
+
+
+_inspect_empty = inspect.Parameter.empty
+
+
+def get_signature_parameters(
+ function: Callable[..., Any],
+ globalns: Optional[Dict[str, Any]] = None,
+ *,
+ skip_standard_params: bool = False,
+) -> Dict[str, inspect.Parameter]:
+ # if no globalns provided, unwrap (where needed) and get global namespace from there
+ if globalns is None:
+ globalns = _get_function_globals(function)
+
+ params: Dict[str, inspect.Parameter] = {}
+ cache: Dict[str, Any] = {}
+
+ signature = inspect.signature(function)
+ iterator = iter(signature.parameters.items())
+
+ if skip_standard_params:
+ # skip `self` (if present) and `ctx` parameters,
+ # since their annotations are irrelevant
+ skip = 2 if signature_has_self_param(function) else 1
+
+ for _ in range(skip):
+ try:
+ next(iterator)
+ except StopIteration:
+ raise ValueError(
+ f"Expected command callback to have at least {skip} parameter(s)"
+ ) from None
+
+ # eval all parameter annotations
+ for name, parameter in iterator:
+ annotation = parameter.annotation
+ if annotation is _inspect_empty:
+ params[name] = parameter
+ continue
+
+ if annotation is None:
+ annotation = type(None)
+ else:
+ annotation = evaluate_annotation(annotation, globalns, globalns, cache)
+
+ params[name] = parameter.replace(annotation=annotation)
+
+ return params
+
+
+def get_signature_return(function: Callable[..., Any]) -> Any:
+ signature = inspect.signature(function)
+
+ # same as parameters above, but for the return annotation
+ ret = signature.return_annotation
+ if ret is not _inspect_empty:
+ if ret is None:
+ ret = type(None)
+ else:
+ globalns = _get_function_globals(function)
+ ret = evaluate_annotation(ret, globalns, globalns, {})
+
+ return ret
+
+
+def signature_has_self_param(function: Callable[..., Any]) -> bool:
+ # If a function was defined in a class and is not bound (i.e. is not types.MethodType),
+ # it should have a `self` parameter.
+ # Bound methods technically also have a `self` parameter, but this is
+ # used in conjunction with `inspect.signature`, which drops that parameter.
+ #
+ # There isn't really any way to reliably detect whether a function
+ # was defined in a class, other than `__qualname__`, thanks to PEP 3155.
+ # As noted in the PEP, this doesn't work with rebinding, but that should be a pretty rare edge case.
+ #
+ #
+ # There are a few possible situations here - for the purposes of this method,
+ # we want to detect the first case only:
+ # (1) The preceding component for *methods in classes* will be the class name, resulting in `Clazz.func`.
+ # (2) For *unbound* functions (not methods), `__qualname__ == __name__`.
+ # (3) Bound methods (i.e. types.MethodType) don't have a `self` parameter in the context of this function (see first paragraph).
+ # (we currently don't expect to handle bound methods anywhere, except the default help command implementation).
+ # (4) A somewhat special case are lambdas defined in a class namespace (but not inside a method), which use `Clazz.` and shouldn't match (1).
+ # (lambdas at class level are a bit funky; we currently only expect them in the `Param(converter=)` kwarg, which doesn't take a `self` parameter).
+ # (5) Similarly, *nested functions* use `containing_func..func` and shouldn't have a `self` parameter.
+ #
+ # Working solely based on this string is certainly not ideal,
+ # but the compiler does a bunch of processing just for that attribute,
+ # and there's really no other way to retrieve this information through other means later.
+ # (3.10: https://github.com/python/cpython/blob/e07086db03d2dc1cd2e2a24f6c9c0ddd422b4cf0/Python/compile.c#L744)
+ #
+ # Not reliable for classmethod/staticmethod.
+
+ qname = function.__qualname__
+ if qname == function.__name__:
+ # (2)
+ return False
+
+ if isinstance(function, types.MethodType):
+ # (3)
+ return False
+
+ # "a.b.c.d" => "a.b.c", "d"
+ parent, basename = qname.rsplit(".", 1)
+
+ if basename == "":
+ # (4)
+ return False
+
+ # (5)
+ return not parent.endswith(".")
+
+
TimestampStyle = Literal["f", "F", "d", "D", "t", "T", "R"]
diff --git a/disnake/voice_client.py b/disnake/voice_client.py
index 52750ecebd..a6cc13e0ba 100644
--- a/disnake/voice_client.py
+++ b/disnake/voice_client.py
@@ -279,7 +279,7 @@ async def on_voice_server_update(self, data: VoiceServerUpdateEvent) -> None:
self.server_id = int(data["guild_id"])
endpoint = data.get("endpoint")
- if endpoint is None or self.token is None:
+ if endpoint is None or not self.token:
_log.warning(
"Awaiting endpoint... This requires waiting. "
"If timeout occurred considering raising the timeout and reconnecting."
diff --git a/disnake/voice_region.py b/disnake/voice_region.py
index 6c957d5e04..b08689db5b 100644
--- a/disnake/voice_region.py
+++ b/disnake/voice_region.py
@@ -14,7 +14,7 @@
class VoiceRegion:
"""Represents a Discord voice region.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/webhook/async_.py b/disnake/webhook/async_.py
index edd9ec3dcd..851a88dfee 100644
--- a/disnake/webhook/async_.py
+++ b/disnake/webhook/async_.py
@@ -63,7 +63,7 @@
from ..mentions import AllowedMentions
from ..message import Attachment
from ..state import ConnectionState
- from ..sticker import GuildSticker, StickerItem
+ from ..sticker import GuildSticker, StandardSticker, StickerItem
from ..types.message import Message as MessagePayload
from ..types.webhook import Webhook as WebhookPayload
from ..ui.action_row import Components
@@ -491,7 +491,7 @@ def handle_message_parameters_dict(
components: Optional[Components[MessageUIComponent]] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
previous_allowed_mentions: Optional[AllowedMentions] = None,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = MISSING,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = MISSING,
thread_name: Optional[str] = None,
) -> DictPayloadParameters:
if files is not MISSING and file is not MISSING:
@@ -578,7 +578,7 @@ def handle_message_parameters(
components: Optional[Components[MessageUIComponent]] = MISSING,
allowed_mentions: Optional[AllowedMentions] = MISSING,
previous_allowed_mentions: Optional[AllowedMentions] = None,
- stickers: Sequence[Union[GuildSticker, StickerItem]] = MISSING,
+ stickers: Sequence[Union[GuildSticker, StandardSticker, StickerItem]] = MISSING,
thread_name: Optional[str] = None,
) -> PayloadParameters:
params = handle_message_parameters_dict(
@@ -1034,7 +1034,7 @@ async def foo():
For a synchronous counterpart, see :class:`SyncWebhook`.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/webhook/sync.py b/disnake/webhook/sync.py
index 0d2ba42b6c..b1debb9cf3 100644
--- a/disnake/webhook/sync.py
+++ b/disnake/webhook/sync.py
@@ -510,7 +510,7 @@ class SyncWebhook(BaseWebhook):
For an asynchronous counterpart, see :class:`Webhook`.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/disnake/widget.py b/disnake/widget.py
index d5056d82dc..4293985a36 100644
--- a/disnake/widget.py
+++ b/disnake/widget.py
@@ -34,7 +34,7 @@
class WidgetChannel:
"""Represents a "partial" widget channel.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -89,7 +89,7 @@ def created_at(self) -> datetime.datetime:
class WidgetMember(BaseUser):
"""Represents a "partial" member of the widget's guild.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
@@ -262,7 +262,7 @@ async def edit(
class Widget:
"""Represents a :class:`Guild` widget.
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/docs/_static/style.css b/docs/_static/style.css
index a54a5ecf52..b89e43a930 100644
--- a/docs/_static/style.css
+++ b/docs/_static/style.css
@@ -1313,21 +1313,20 @@ rect.highlighted {
fill: var(--highlighted-text);
}
-.container.operations {
+details.operations {
padding: 10px;
border: 1px solid var(--codeblock-border);
margin-bottom: 20px;
}
-.container.operations::before {
- content: 'Supported Operations';
- color: var(--main-big-headers-text);
- display: block;
- padding-bottom: 0.5em;
+details.operations dl {
+ margin-top: 15px;
+ margin-bottom: 15px;
}
-.container.operations > dl.describe > dt {
- background-color: var(--api-entry-background);
+details.operations > summary::after {
+ content: 'Supported Operations';
+ color: var(--main-big-headers-text);
}
.table-wrapper {
diff --git a/docs/api/app_commands.rst b/docs/api/app_commands.rst
index 1d26bee539..a55e3670ab 100644
--- a/docs/api/app_commands.rst
+++ b/docs/api/app_commands.rst
@@ -97,7 +97,7 @@ Option
.. attributetable:: Option
-.. autoclass:: Option()
+.. autoclass:: Option
:members:
OptionChoice
diff --git a/docs/api/app_info.rst b/docs/api/app_info.rst
index eed2a74143..7aa6c4148f 100644
--- a/docs/api/app_info.rst
+++ b/docs/api/app_info.rst
@@ -49,6 +49,7 @@ TeamMember
.. autoclass:: TeamMember()
:members:
+ :inherited-members:
Data Classes
------------
@@ -89,6 +90,27 @@ TeamMembershipState
Represents a member currently in the team.
+TeamMemberRole
+~~~~~~~~~~~~~~
+
+.. class:: TeamMemberRole
+
+ Represents the role of a team member retrieved through :func:`Client.application_info`.
+
+ .. versionadded:: 2.10
+
+ .. attribute:: admin
+
+ Admins have the most permissions. An admin can only take destructive actions on the team or team-owned apps if they are the team owner.
+
+ .. attribute:: developer
+
+ Developers can access information about a team and team-owned applications, and take limited actions on them, like configuring interaction endpoints or resetting the bot token.
+
+ .. attribute:: read_only
+
+ Read-only members can access information about a team and team-owned applications.
+
ApplicationRoleConnectionMetadataType
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/api/audit_logs.rst b/docs/api/audit_logs.rst
index 1052c610b4..e4cb0573d2 100644
--- a/docs/api/audit_logs.rst
+++ b/docs/api/audit_logs.rst
@@ -93,7 +93,7 @@ AuditLogDiff
on the action being done, check the documentation for :class:`AuditLogAction`,
otherwise check the documentation below for all attributes that are possible.
- .. container:: operations
+ .. collapse:: operations
.. describe:: iter(diff)
diff --git a/docs/api/guilds.rst b/docs/api/guilds.rst
index 85f8d3d494..72a7ad6f84 100644
--- a/docs/api/guilds.rst
+++ b/docs/api/guilds.rst
@@ -131,7 +131,7 @@ VerificationLevel
Specifies a :class:`Guild`\'s verification level, which is the criteria in
which a member must meet before being able to send messages to the guild.
- .. container:: operations
+ .. collapse:: operations
.. versionadded:: 2.0
@@ -180,9 +180,7 @@ NotificationLevel
Specifies whether a :class:`Guild` has notifications on for all messages or mentions only by default.
- .. container:: operations
-
- .. versionadded:: 2.0
+ .. collapse:: operations
.. describe:: x == y
@@ -219,9 +217,7 @@ ContentFilter
learning algorithms that Discord uses to detect if an image contains
NSFW content.
- .. container:: operations
-
- .. versionadded:: 2.0
+ .. collapse:: operations
.. describe:: x == y
@@ -261,7 +257,7 @@ NSFWLevel
.. versionadded:: 2.0
- .. container:: operations
+ .. collapse:: operations
.. describe:: x == y
diff --git a/docs/api/messages.rst b/docs/api/messages.rst
index 123260f167..3031d955d9 100644
--- a/docs/api/messages.rst
+++ b/docs/api/messages.rst
@@ -188,14 +188,14 @@ MessageType
Specifies the type of :class:`Message`. This is used to denote if a message
is to be interpreted as a system message or a regular message.
- .. container:: operations
+ .. collapse:: operations
- .. describe:: x == y
+ .. describe:: x == y
- Checks if two messages are equal.
- .. describe:: x != y
+ Checks if two messages are equal.
+ .. describe:: x != y
- Checks if two messages are not equal.
+ Checks if two messages are not equal.
.. attribute:: default
diff --git a/docs/api/misc.rst b/docs/api/misc.rst
index c49854c289..83ee3298d8 100644
--- a/docs/api/misc.rst
+++ b/docs/api/misc.rst
@@ -18,7 +18,7 @@ AsyncIterator
Represents the "AsyncIterator" concept. Note that no such class exists,
it is purely abstract.
- .. container:: operations
+ .. collapse:: operations
.. describe:: async for x in y
diff --git a/docs/api/ui.rst b/docs/api/ui.rst
index 85c85e37c4..c7c061f137 100644
--- a/docs/api/ui.rst
+++ b/docs/api/ui.rst
@@ -128,7 +128,7 @@ TextInput
Functions
---------
-.. autofunction:: button(cls=Button, *, style=ButtonStyle.secondary, label=None, disabled=False, custom_id=..., url=None, emoji=None, row=None)
+.. autofunction:: button(cls=Button, *, custom_id=..., style=ButtonStyle.secondary, label=None, disabled=False, url=None, emoji=None, row=None)
:decorator:
.. autofunction:: string_select(cls=StringSelect, *, custom_id=..., placeholder=None, min_values=1, max_values=1, options=..., disabled=False, row=None)
diff --git a/docs/conf.py b/docs/conf.py
index 355f977465..5944191079 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -52,6 +52,7 @@
"exception_hierarchy",
"attributetable",
"resourcelinks",
+ "collapse",
"nitpick_file_ignorer",
]
diff --git a/docs/ext/commands/api/checks.rst b/docs/ext/commands/api/checks.rst
index 336b920c14..2ced115197 100644
--- a/docs/ext/commands/api/checks.rst
+++ b/docs/ext/commands/api/checks.rst
@@ -63,6 +63,12 @@ Functions
.. autofunction:: check_any(*checks)
:decorator:
+.. autofunction:: app_check(predicate)
+ :decorator:
+
+.. autofunction:: app_check_any(*checks)
+ :decorator:
+
.. autofunction:: has_role(item)
:decorator:
diff --git a/docs/extensions/attributetable.py b/docs/extensions/attributetable.py
index 1d6a2cb663..d718c73604 100644
--- a/docs/extensions/attributetable.py
+++ b/docs/extensions/attributetable.py
@@ -6,7 +6,7 @@
import inspect
import re
from collections import defaultdict
-from typing import TYPE_CHECKING, DefaultDict, Dict, List, NamedTuple, Optional, Tuple
+from typing import TYPE_CHECKING, ClassVar, DefaultDict, Dict, List, NamedTuple, Optional, Tuple
from docutils import nodes
from sphinx import addnodes
@@ -14,11 +14,13 @@
from sphinx.util.docutils import SphinxDirective
if TYPE_CHECKING:
- from _types import SphinxExtensionMeta
from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment
+ from sphinx.util.typing import OptionSpec
from sphinx.writers.html import HTMLTranslator
+ from ._types import SphinxExtensionMeta
+
class attributetable(nodes.General, nodes.Element):
pass
@@ -100,7 +102,7 @@ class PyAttributeTable(SphinxDirective):
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
- option_spec = {}
+ option_spec: ClassVar[OptionSpec] = {}
def parse_name(self, content: str) -> Tuple[str, Optional[str]]:
match = _name_parser_regex.match(content)
diff --git a/docs/extensions/builder.py b/docs/extensions/builder.py
index 6f1a5493d4..61e366d2ca 100644
--- a/docs/extensions/builder.py
+++ b/docs/extensions/builder.py
@@ -8,12 +8,13 @@
from sphinx.environment.adapters.indexentries import IndexEntries
if TYPE_CHECKING:
- from _types import SphinxExtensionMeta
from docutils import nodes
from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.writers.html5 import HTML5Translator
+ from ._types import SphinxExtensionMeta
+
if TYPE_CHECKING:
translator_base = HTML5Translator
else:
@@ -64,7 +65,7 @@ def disable_mathjax(app: Sphinx, config: Config) -> None:
# inspired by https://github.com/readthedocs/sphinx-hoverxref/blob/003b84fee48262f1a969c8143e63c177bd98aa26/hoverxref/extension.py#L151
for listener in app.events.listeners.get("html-page-context", []):
- module_name = inspect.getmodule(listener.handler).__name__ # type: ignore
+ module_name = inspect.getmodule(listener.handler).__name__
if module_name == "sphinx.ext.mathjax":
app.disconnect(listener.id)
diff --git a/docs/extensions/collapse.py b/docs/extensions/collapse.py
new file mode 100644
index 0000000000..cde568aea2
--- /dev/null
+++ b/docs/extensions/collapse.py
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: MIT
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, ClassVar
+
+from docutils import nodes
+from docutils.parsers.rst import Directive, directives
+
+if TYPE_CHECKING:
+ from sphinx.application import Sphinx
+ from sphinx.util.typing import OptionSpec
+ from sphinx.writers.html import HTMLTranslator
+
+ from ._types import SphinxExtensionMeta
+
+
+class collapse(nodes.General, nodes.Element):
+ pass
+
+
+def visit_collapse_node(self: HTMLTranslator, node: nodes.Element) -> None:
+ attrs = {"open": ""} if node["open"] else {}
+ self.body.append(self.starttag(node, "details", **attrs))
+ self.body.append("")
+
+
+def depart_collapse_node(self: HTMLTranslator, node: nodes.Element) -> None:
+ self.body.append("\n")
+
+
+class CollapseDirective(Directive):
+ has_content = True
+
+ optional_arguments = 1
+ final_argument_whitespace = True
+
+ option_spec: ClassVar[OptionSpec] = {"open": directives.flag}
+
+ def run(self):
+ self.assert_has_content()
+ node = collapse(
+ "\n".join(self.content),
+ open="open" in self.options,
+ )
+
+ classes = directives.class_option(self.arguments[0] if self.arguments else "")
+ node["classes"].extend(classes)
+
+ self.state.nested_parse(self.content, self.content_offset, node)
+ return [node]
+
+
+def setup(app: Sphinx) -> SphinxExtensionMeta:
+ app.add_node(collapse, html=(visit_collapse_node, depart_collapse_node))
+ app.add_directive("collapse", CollapseDirective)
+
+ return {
+ "parallel_read_safe": True,
+ "parallel_write_safe": True,
+ }
diff --git a/docs/extensions/exception_hierarchy.py b/docs/extensions/exception_hierarchy.py
index 147a175af2..67040643ae 100644
--- a/docs/extensions/exception_hierarchy.py
+++ b/docs/extensions/exception_hierarchy.py
@@ -7,10 +7,11 @@
from docutils.parsers.rst import Directive
if TYPE_CHECKING:
- from _types import SphinxExtensionMeta
from sphinx.application import Sphinx
from sphinx.writers.html import HTMLTranslator
+ from ._types import SphinxExtensionMeta
+
class exception_hierarchy(nodes.General, nodes.Element):
pass
diff --git a/docs/extensions/fulltoc.py b/docs/extensions/fulltoc.py
index 1d7523e52a..e35cd79514 100644
--- a/docs/extensions/fulltoc.py
+++ b/docs/extensions/fulltoc.py
@@ -31,7 +31,6 @@
from typing import TYPE_CHECKING, List, cast
-from _types import SphinxExtensionMeta
from docutils import nodes
from sphinx import addnodes
@@ -40,6 +39,8 @@
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.environment import BuildEnvironment
+ from ._types import SphinxExtensionMeta
+
# {prefix: index_doc} mapping
# Any document that matches `prefix` will use `index_doc`'s toctree instead.
GROUPED_SECTIONS = {"api/": "api/index", "ext/commands/api/": "ext/commands/api/index"}
diff --git a/docs/extensions/nitpick_file_ignorer.py b/docs/extensions/nitpick_file_ignorer.py
index da967f9d92..cc9eab588f 100644
--- a/docs/extensions/nitpick_file_ignorer.py
+++ b/docs/extensions/nitpick_file_ignorer.py
@@ -7,9 +7,10 @@
from sphinx.util import logging as sphinx_logging
if TYPE_CHECKING:
- from _types import SphinxExtensionMeta
from sphinx.application import Sphinx
+ from ._types import SphinxExtensionMeta
+
class NitpickFileIgnorer(logging.Filter):
def __init__(self, app: Sphinx) -> None:
diff --git a/docs/extensions/redirects.py b/docs/extensions/redirects.py
index fea63483be..44d1c16bef 100644
--- a/docs/extensions/redirects.py
+++ b/docs/extensions/redirects.py
@@ -1,13 +1,16 @@
# SPDX-License-Identifier: MIT
+from __future__ import annotations
import json
from pathlib import Path
-from typing import Dict
+from typing import TYPE_CHECKING, Dict
-from _types import SphinxExtensionMeta
from sphinx.application import Sphinx
from sphinx.util.fileutil import copy_asset_file
+if TYPE_CHECKING:
+ from ._types import SphinxExtensionMeta
+
SCRIPT_PATH = "_templates/api_redirect.js_t"
diff --git a/docs/extensions/resourcelinks.py b/docs/extensions/resourcelinks.py
index 76a57b4656..d93f6f2715 100644
--- a/docs/extensions/resourcelinks.py
+++ b/docs/extensions/resourcelinks.py
@@ -10,12 +10,13 @@
from sphinx.util.nodes import split_explicit_title
if TYPE_CHECKING:
- from _types import SphinxExtensionMeta
from docutils.nodes import Node, system_message
from docutils.parsers.rst.states import Inliner
from sphinx.application import Sphinx
from sphinx.util.typing import RoleFunction
+ from ._types import SphinxExtensionMeta
+
def make_link_role(resource_links: Dict[str, str]) -> RoleFunction:
def role(
diff --git a/docs/whats_new.rst b/docs/whats_new.rst
index d1d3064891..d9bd644863 100644
--- a/docs/whats_new.rst
+++ b/docs/whats_new.rst
@@ -17,6 +17,22 @@ in specific versions. Please see :ref:`version_guarantees` for more information.
.. towncrier release notes start
+.. _vp2p9p1:
+
+v2.9.1
+------
+
+Bug Fixes
+~~~~~~~~~
+- Allow ``cls`` argument in select menu decorators (e.g. :func:`ui.string_select`) to be specified by keyword instead of being positional-only. (:issue:`1111`)
+- |commands| Fix edge case in evaluation of multiple identical annotations with forwardrefs in a single signature. (:issue:`1120`)
+- Fix :meth:`Thread.permissions_for` not working in some cases due to an incorrect import. (:issue:`1123`)
+
+Documentation
+~~~~~~~~~~~~~
+- Miscellaneous grammar/typo fixes for :doc:`api/audit_logs`. (:issue:`1105`)
+
+
.. _vp2p9p0:
v2.9.0
diff --git a/examples/basic_voice.py b/examples/basic_voice.py
index 6d224b21e5..45046c780f 100644
--- a/examples/basic_voice.py
+++ b/examples/basic_voice.py
@@ -33,8 +33,6 @@
"source_address": "0.0.0.0", # bind to ipv4 since ipv6 addresses cause issues sometimes
}
-ffmpeg_options = {"options": "-vn"}
-
ytdl = youtube_dl.YoutubeDL(ytdl_format_options)
@@ -59,7 +57,7 @@ async def from_url(
filename = data["url"] if stream else ytdl.prepare_filename(data)
- return cls(disnake.FFmpegPCMAudio(filename, **ffmpeg_options), data=data)
+ return cls(disnake.FFmpegPCMAudio(filename, options="-vn"), data=data)
class Music(commands.Cog):
diff --git a/examples/interactions/injections.py b/examples/interactions/injections.py
index 27576d60bc..30c7554dd6 100644
--- a/examples/interactions/injections.py
+++ b/examples/interactions/injections.py
@@ -114,7 +114,7 @@ async def get_game_user(
if user is None:
return await db.get_game_user(id=inter.author.id)
- game_user: GameUser = await db.search_game_user(username=user, server=server)
+ game_user: Optional[GameUser] = await db.search_game_user(username=user, server=server)
if game_user is None:
raise commands.CommandError(f"User with username {user!r} could not be found")
diff --git a/examples/interactions/modal.py b/examples/interactions/modal.py
index 00b2364789..f271c82f4c 100644
--- a/examples/interactions/modal.py
+++ b/examples/interactions/modal.py
@@ -2,6 +2,8 @@
"""An example demonstrating two methods of sending modals and handling modal responses."""
+# pyright: reportUnknownLambdaType=false
+
import asyncio
import os
diff --git a/noxfile.py b/noxfile.py
index 5ed870cb12..326ae81f6d 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -24,8 +24,6 @@
"PDM_IGNORE_SAVED_PYTHON": "1",
},
)
-# support the python parser in case the native parser isn't available
-os.environ.setdefault("LIBCST_PARSER_TYPE", "native")
nox.options.error_on_external_run = True
@@ -204,7 +202,7 @@ def pyright(session: nox.Session) -> None:
pass
-@nox.session(python=["3.8", "3.9", "3.10", "3.11"])
+@nox.session(python=["3.8", "3.9", "3.10", "3.11", "3.12"])
@nox.parametrize(
"extras",
[
diff --git a/pyproject.toml b/pyproject.toml
index 369abf8c04..73f3ad1b9f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,30 +70,37 @@ tools = [
"pre-commit~=3.0",
"slotscheck~=0.16.4",
"python-dotenv~=1.0.0",
- "towncrier==23.6.0",
"check-manifest==0.49",
- "ruff==0.0.261",
+ "ruff==0.0.292",
+]
+changelog = [
+ "towncrier==23.6.0",
]
codemod = [
# run codemods on the respository (mostly automated typing)
- "libcst~=0.4.9",
+ "libcst~=1.1.0",
"black==23.9.1",
"autotyping==23.2.0",
]
typing = [
# this is not pyright itself, but the python wrapper
- "pyright==1.1.291",
- "typing-extensions~=4.5.0",
+ "pyright==1.1.336",
+ "typing-extensions~=4.8.0",
# only used for type-checking, version does not matter
"pytz",
]
test = [
- "pytest~=7.2.1",
+ "pytest~=7.4.2",
"pytest-cov~=4.0.0",
"pytest-asyncio~=0.20.3",
"looptime~=0.2",
"coverage[toml]~=6.5.0",
]
+build = [
+ "wheel~=0.40.0",
+ "build~=0.10.0",
+ "twine~=4.0.2",
+]
[tool.pdm.scripts]
black = { composite = ["lint black"], help = "Run black" }
@@ -120,7 +127,7 @@ runner = "pdm run"
[tool.black]
line-length = 100
-target-version = ["py38", "py39", "py310", "py311"]
+target-version = ["py38", "py39", "py310", "py311", "py312"]
[tool.isort]
profile = "black"
@@ -149,7 +156,7 @@ select = [
# "RET", # flake8-return
# "SIM", # flake8-simplify
"TID251", # flake8-tidy-imports, replaces S404
- # "TCH", # flake8-type-checking
+ "TCH", # flake8-type-checking
"RUF", # ruff specific exceptions
"PT", # flake8-pytest-style
"Q", # flake8-quotes
@@ -177,19 +184,34 @@ ignore = [
"RUF005", # might not be actually faster
"RUF006", # might not be an issue/very extreme cases
+ # calling subprocess with dynamic arguments is generally fine, the only way to avoid this is ignoring it
+ "S603",
+
+ # partial executable paths (i.e. "git" instead of "/usr/bin/git") are fine
+ "S607",
+
# ignore try-except-pass. Bare excepts are caught with E722
"S110",
# provide specific codes on type: ignore
"PGH003",
+ # typevar names don't match variance (we don't always want this)
+ "PLC0105",
+
# import aliases are fixed by isort
"PLC0414",
-
# outer loop variables are overwritten by inner assignment target, these are mostly intentional
"PLW2901",
+ # ignore imports that could be moved into type-checking blocks
+ # (no real advantage other than possibly avoiding cycles,
+ # but can be dangerous in places where we need to parse signatures)
+ "TCH001",
+ "TCH002",
+ "TCH003",
+
# temporary disables, to fix later
"D205", # blank line required between summary and description
"D401", # first line of docstring should be in imperative mood
@@ -215,8 +237,8 @@ ignore = [
"T201", # print found, printing is currently accepted in the test bot
"PT", # this is not a module of pytest tests
]
-"scripts/*.py" = ["S101"] # use of assert is okay in scripts
"tests/*.py" = ["S101"] # use of assert is okay in test files
+"scripts/*.py" = ["S101"] # use of assert is okay in scripts
# we are not using noqa in the example files themselves
"examples/*.py" = [
"B008", # do not perform function calls in argument defaults, this is how most commands work
@@ -363,6 +385,7 @@ ignore = [
"noxfile.py",
# docs
"CONTRIBUTING.md",
+ "RELEASE.md",
"assets/**",
"changelog/**",
"docs/**",
diff --git a/scripts/ci/versiontool.py b/scripts/ci/versiontool.py
new file mode 100644
index 0000000000..cec50cff3d
--- /dev/null
+++ b/scripts/ci/versiontool.py
@@ -0,0 +1,128 @@
+# SPDX-License-Identifier: MIT
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+from enum import Enum
+from pathlib import Path
+from typing import NamedTuple, NoReturn
+
+TARGET_FILE = Path("disnake/__init__.py")
+ORIG_INIT_CONTENTS = TARGET_FILE.read_text("utf-8")
+
+version_re = re.compile(r"(\d+)\.(\d+)\.(\d+)(?:(a|b|rc)(\d+)?)?")
+
+
+class ReleaseLevel(Enum):
+ alpha = "a"
+ beta = "b"
+ candidate = "rc"
+ final = ""
+
+
+class VersionInfo(NamedTuple):
+ major: int
+ minor: int
+ micro: int
+ releaselevel: ReleaseLevel
+ serial: int
+
+ @classmethod
+ def from_str(cls, s: str) -> VersionInfo:
+ match = version_re.fullmatch(s)
+ if not match:
+ raise ValueError(f"invalid version: '{s}'")
+
+ major, minor, micro, releaselevel, serial = match.groups()
+ return VersionInfo(
+ int(major),
+ int(minor),
+ int(micro),
+ ReleaseLevel(releaselevel or ""),
+ int(serial or 0),
+ )
+
+ def __str__(self) -> str:
+ s = f"{self.major}.{self.minor}.{self.micro}"
+ if self.releaselevel is not ReleaseLevel.final:
+ s += self.releaselevel.value
+ if self.serial:
+ s += str(self.serial)
+ return s
+
+ def to_versioninfo(self) -> str:
+ return (
+ f"VersionInfo(major={self.major}, minor={self.minor}, micro={self.micro}, "
+ f'releaselevel="{self.releaselevel.name}", serial={self.serial})'
+ )
+
+
+def get_current_version() -> VersionInfo:
+ match = re.search(r"^__version__\b.*\"(.+?)\"$", ORIG_INIT_CONTENTS, re.MULTILINE)
+ assert match, "could not find current version in __init__.py"
+ return VersionInfo.from_str(match[1])
+
+
+def replace_line(text: str, regex: str, repl: str) -> str:
+ lines = []
+ found = False
+
+ for line in text.split("\n"):
+ if re.search(regex, line):
+ found = True
+ line = repl
+ lines.append(line)
+
+ assert found, f"failed to find `{regex}` in file"
+ return "\n".join(lines)
+
+
+def fail(msg: str) -> NoReturn:
+ print("error:", msg, file=sys.stderr)
+ sys.exit(1)
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument("--set", metavar="VERSION", help="set new version (e.g. '1.2.3' or 'dev')")
+ group.add_argument("--show", action="store_true", help="print current version")
+ args = parser.parse_args()
+
+ current_version = get_current_version()
+
+ if args.show:
+ print(str(current_version))
+ return
+
+ # else, update to specified version
+ new_version_str = args.set
+
+ if new_version_str == "dev":
+ if current_version.releaselevel is not ReleaseLevel.final:
+ fail("Current version must be final to bump to dev version")
+ new_version = VersionInfo(
+ major=current_version.major,
+ minor=current_version.minor + 1,
+ micro=0,
+ releaselevel=ReleaseLevel.alpha,
+ serial=0,
+ )
+ else:
+ new_version = VersionInfo.from_str(new_version_str)
+
+ text = ORIG_INIT_CONTENTS
+ text = replace_line(text, r"^__version__\b", f'__version__ = "{new_version!s}"')
+ text = replace_line(
+ text, r"^version_info\b", f"version_info: VersionInfo = {new_version.to_versioninfo()}"
+ )
+
+ if text != ORIG_INIT_CONTENTS:
+ TARGET_FILE.write_text(text, "utf-8")
+
+ print(str(new_version))
+
+
+main()
diff --git a/test_bot/cogs/modals.py b/test_bot/cogs/modals.py
index 13c84bddf2..c5d514a25c 100644
--- a/test_bot/cogs/modals.py
+++ b/test_bot/cogs/modals.py
@@ -65,7 +65,7 @@ async def create_tag_low(self, inter: disnake.AppCmdInter[commands.Bot]) -> None
modal_inter: disnake.ModalInteraction = await self.bot.wait_for(
"modal_submit",
- check=lambda i: i.custom_id == "create_tag2" and i.author.id == inter.author.id,
+ check=lambda i: i.custom_id == "create_tag2" and i.author.id == inter.author.id, # type: ignore # unknown parameter type
)
embed = disnake.Embed(title="Tag Creation")
diff --git a/tests/ext/commands/test_base_core.py b/tests/ext/commands/test_base_core.py
index ccb15f42fc..734353c5a4 100644
--- a/tests/ext/commands/test_base_core.py
+++ b/tests/ext/commands/test_base_core.py
@@ -2,6 +2,7 @@
import pytest
+import disnake
from disnake import Permissions
from disnake.ext import commands
@@ -82,3 +83,20 @@ async def overwrite_decorator_below(self, _) -> None:
assert Cog.overwrite_decorator_below.default_member_permissions == Permissions(64)
assert Cog().overwrite_decorator_below.default_member_permissions == Permissions(64)
+
+
+def test_localization_copy() -> None:
+ class Cog(commands.Cog):
+ @commands.slash_command()
+ async def cmd(
+ self,
+ inter,
+ param: int = commands.Param(name=disnake.Localized("param", key="PARAM")),
+ ) -> None:
+ ...
+
+ # Ensure the command copy that happens on cog init doesn't raise a LocalizationWarning for the options.
+ cog = Cog()
+
+ with pytest.warns(disnake.LocalizationWarning):
+ assert cog.get_slash_commands()[0].options[0].name_localizations.data is None
diff --git a/tests/ext/commands/test_core.py b/tests/ext/commands/test_core.py
index 1d3076a845..2b29f51988 100644
--- a/tests/ext/commands/test_core.py
+++ b/tests/ext/commands/test_core.py
@@ -1,20 +1,10 @@
# SPDX-License-Identifier: MIT
-from typing import TYPE_CHECKING
+from typing_extensions import assert_type
from disnake.ext import commands
from tests.helpers import reveal_type
-if TYPE_CHECKING:
- from typing_extensions import assert_type
-
- # NOTE: using undocumented `expected_text` parameter of pyright instead of `assert_type`,
- # as `assert_type` can't handle bound ParamSpecs
- reveal_type(
- 42, # type: ignore
- expected_text="str", # type: ignore
- )
-
class CustomContext(commands.Context):
...
diff --git a/tests/ext/commands/test_params.py b/tests/ext/commands/test_params.py
index a3b4ea4289..96f2c08c32 100644
--- a/tests/ext/commands/test_params.py
+++ b/tests/ext/commands/test_params.py
@@ -189,7 +189,6 @@ def test_string(self) -> None:
assert info.max_value is None
assert info.type == annotation.underlying_type
- # uses lambdas since new union syntax isn't supported on all versions
@pytest.mark.parametrize(
"annotation_str",
[
@@ -213,3 +212,65 @@ def test_optional(self, annotation_str) -> None:
assert info.min_value == 1
assert info.max_value == 2
assert info.type == int
+
+
+class TestIsolateSelf:
+ def test_function_simple(self) -> None:
+ def func(a: int) -> None:
+ ...
+
+ (cog, inter), params = commands.params.isolate_self(func)
+ assert cog is None
+ assert inter is None
+ assert params.keys() == {"a"}
+
+ def test_function_inter(self) -> None:
+ def func(inter: disnake.ApplicationCommandInteraction, a: int) -> None:
+ ...
+
+ (cog, inter), params = commands.params.isolate_self(func)
+ assert cog is None # should not be set
+ assert inter is not None
+ assert params.keys() == {"a"}
+
+ def test_unbound_method(self) -> None:
+ class Cog(commands.Cog):
+ def func(self, inter: disnake.ApplicationCommandInteraction, a: int) -> None:
+ ...
+
+ (cog, inter), params = commands.params.isolate_self(Cog.func)
+ assert cog is not None # *should* be set here
+ assert inter is not None
+ assert params.keys() == {"a"}
+
+ # I don't think the param parsing logic ever handles bound methods, but testing for regressions anyway
+ def test_bound_method(self) -> None:
+ class Cog(commands.Cog):
+ def func(self, inter: disnake.ApplicationCommandInteraction, a: int) -> None:
+ ...
+
+ (cog, inter), params = commands.params.isolate_self(Cog().func)
+ assert cog is None # should not be set here, since method is already bound
+ assert inter is not None
+ assert params.keys() == {"a"}
+
+ def test_generic(self) -> None:
+ def func(inter: disnake.ApplicationCommandInteraction[commands.Bot], a: int) -> None:
+ ...
+
+ (cog, inter), params = commands.params.isolate_self(func)
+ assert cog is None
+ assert inter is not None
+ assert params.keys() == {"a"}
+
+ def test_inter_union(self) -> None:
+ def func(
+ inter: Union[commands.Context, disnake.ApplicationCommandInteraction[commands.Bot]],
+ a: int,
+ ) -> None:
+ ...
+
+ (cog, inter), params = commands.params.isolate_self(func)
+ assert cog is None
+ assert inter is not None
+ assert params.keys() == {"a"}
diff --git a/tests/helpers.py b/tests/helpers.py
index 2d5a4d8e41..8e22e0cd08 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -16,6 +16,15 @@ def reveal_type(*args, **kwargs) -> None:
raise RuntimeError
+if TYPE_CHECKING:
+ # NOTE: using undocumented `expected_text` parameter of pyright instead of `assert_type`,
+ # as `assert_type` can't handle bound ParamSpecs
+ reveal_type(
+ 42, # type: ignore # suppress "revealed type is ..." output
+ expected_text="str", # type: ignore # ensure the functionality we want still works as expected
+ )
+
+
CallableT = TypeVar("CallableT", bound=Callable)
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 8c5ee4ec38..46237c2019 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,13 +2,14 @@
import asyncio
import datetime
+import functools
import inspect
import os
import sys
import warnings
from dataclasses import dataclass
from datetime import timedelta, timezone
-from typing import Any, Dict, List, Literal, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, TypeVar, Union
from unittest import mock
import pytest
@@ -17,7 +18,13 @@
import disnake
from disnake import utils
-from . import helpers
+from . import helpers, utils_helper_module
+
+if TYPE_CHECKING:
+ from typing_extensions import TypeAliasType
+elif sys.version_info >= (3, 12):
+ # non-3.12 tests shouldn't be using this
+ from typing import TypeAliasType
def test_missing() -> None:
@@ -377,7 +384,7 @@ class C(A):
__slots__ = {"c": "uwu"}
class D(B, C):
- __slots__ = "xyz"
+ __slots__ = "xyz" # noqa: PLC0205 # this is intentional
assert list(utils.get_slots(D)) == ["a", "a2", "c", "xyz"]
@@ -758,8 +765,8 @@ def test_normalise_optional_params(params, expected) -> None:
("Tuple[dict, List[Literal[42, 99]]]", Tuple[dict, List[Literal[42, 99]]], True),
# 3.10 union syntax
pytest.param(
- "int | Literal[False]",
- Union[int, Literal[False]],
+ "int | float",
+ Union[int, float],
True,
marks=pytest.mark.skipif(sys.version_info < (3, 10), reason="syntax requires py3.10"),
),
@@ -784,6 +791,65 @@ def test_resolve_annotation_literal() -> None:
utils.resolve_annotation(Literal[timezone.utc, 3], globals(), locals(), {}) # type: ignore
+@pytest.mark.skipif(sys.version_info < (3, 12), reason="syntax requires py3.12")
+class TestResolveAnnotationTypeAliasType:
+ def test_simple(self) -> None:
+ # this is equivalent to `type CoolList = List[int]`
+ CoolList = TypeAliasType("CoolList", List[int])
+ assert utils.resolve_annotation(CoolList, globals(), locals(), {}) == List[int]
+
+ def test_generic(self) -> None:
+ # this is equivalent to `type CoolList[T] = List[T]; CoolList[int]`
+ T = TypeVar("T")
+ CoolList = TypeAliasType("CoolList", List[T], type_params=(T,))
+
+ annotation = CoolList[int]
+ assert utils.resolve_annotation(annotation, globals(), locals(), {}) == List[int]
+
+ # alias and arg in local scope
+ def test_forwardref_local(self) -> None:
+ T = TypeVar("T")
+ IntOrStr = Union[int, str]
+ CoolList = TypeAliasType("CoolList", List[T], type_params=(T,))
+
+ annotation = CoolList["IntOrStr"]
+ assert utils.resolve_annotation(annotation, globals(), locals(), {}) == List[IntOrStr]
+
+ # alias and arg in other module scope
+ def test_forwardref_module(self) -> None:
+ resolved = utils.resolve_annotation(
+ utils_helper_module.ListWithForwardRefAlias, globals(), locals(), {}
+ )
+ assert resolved == List[Union[int, str]]
+
+ # combination of the previous two, alias in other module scope and arg in local scope
+ def test_forwardref_mixed(self) -> None:
+ LocalIntOrStr = Union[int, str]
+
+ annotation = utils_helper_module.GenericListAlias["LocalIntOrStr"]
+ assert utils.resolve_annotation(annotation, globals(), locals(), {}) == List[LocalIntOrStr]
+
+ # two different forwardrefs with same name
+ def test_forwardref_duplicate(self) -> None:
+ DuplicateAlias = int
+
+ # first, resolve an annotation where `DuplicateAlias` resolves to the local int
+ cache = {}
+ assert (
+ utils.resolve_annotation(List["DuplicateAlias"], globals(), locals(), cache)
+ == List[int]
+ )
+
+ # then, resolve an annotation where the globalns changes and `DuplicateAlias` resolves to something else
+ # (i.e. this should not resolve to `List[int]` despite {"DuplicateAlias": int} in the cache)
+ assert (
+ utils.resolve_annotation(
+ utils_helper_module.ListWithDuplicateAlias, globals(), locals(), cache
+ )
+ == List[str]
+ )
+
+
@pytest.mark.parametrize(
("dt", "style", "expected"),
[
@@ -880,3 +946,84 @@ def test_as_valid_locale(locale, expected) -> None:
)
def test_humanize_list(values, expected) -> None:
assert utils.humanize_list(values, "plus") == expected
+
+
+# used for `test_signature_has_self_param`
+def _toplevel():
+ def inner() -> None:
+ ...
+
+ return inner
+
+
+def decorator(f):
+ @functools.wraps(f)
+ def wrap(self, *args, **kwargs):
+ return f(self, *args, **kwargs)
+
+ return wrap
+
+
+# used for `test_signature_has_self_param`
+class _Clazz:
+ def func(self):
+ def inner() -> None:
+ ...
+
+ return inner
+
+ @classmethod
+ def cmethod(cls) -> None:
+ ...
+
+ @staticmethod
+ def smethod() -> None:
+ ...
+
+ class Nested:
+ def func(self):
+ def inner() -> None:
+ ...
+
+ return inner
+
+ rebind = _toplevel
+
+ @decorator
+ def decorated(self) -> None:
+ ...
+
+ _lambda = lambda: None
+
+
+@pytest.mark.parametrize(
+ ("function", "expected"),
+ [
+ # top-level function
+ (_toplevel, False),
+ # methods in class
+ (_Clazz.func, True),
+ (_Clazz().func, False),
+ # unfortunately doesn't work
+ (_Clazz.rebind, False),
+ (_Clazz().rebind, False),
+ # classmethod/staticmethod isn't supported, but checked to ensure consistency
+ (_Clazz.cmethod, False),
+ (_Clazz.smethod, True),
+ # nested class methods
+ (_Clazz.Nested.func, True),
+ (_Clazz.Nested().func, False),
+ # inner methods
+ (_toplevel(), False),
+ (_Clazz().func(), False),
+ (_Clazz.Nested().func(), False),
+ # decorated method
+ (_Clazz.decorated, True),
+ (_Clazz().decorated, False),
+ # lambda (class-level)
+ (_Clazz._lambda, False),
+ (_Clazz()._lambda, False),
+ ],
+)
+def test_signature_has_self_param(function, expected) -> None:
+ assert utils.signature_has_self_param(function) == expected
diff --git a/tests/ui/test_action_row.py b/tests/ui/test_action_row.py
index 9e72ecc3eb..f9c40ffedc 100644
--- a/tests/ui/test_action_row.py
+++ b/tests/ui/test_action_row.py
@@ -4,17 +4,20 @@
from unittest import mock
import pytest
+from typing_extensions import assert_type
import disnake
-from disnake.ui import ActionRow, Button, StringSelect, TextInput, WrappedComponent
+from disnake.ui import (
+ ActionRow,
+ Button,
+ MessageUIComponent,
+ ModalUIComponent,
+ StringSelect,
+ TextInput,
+ WrappedComponent,
+)
from disnake.ui.action_row import components_to_dict, components_to_rows
-if TYPE_CHECKING:
- from typing_extensions import assert_type
-
- from disnake.ui import MessageUIComponent, ModalUIComponent
-
-
button1 = Button()
button2 = Button()
button3 = Button()
@@ -133,9 +136,8 @@ def test_with_components(self) -> None:
row_msg = ActionRow.with_message_components()
assert list(row_msg.children) == []
- if TYPE_CHECKING:
- assert_type(row_modal, ActionRow[ModalUIComponent])
- assert_type(row_msg, ActionRow[MessageUIComponent])
+ assert_type(row_modal, ActionRow[ModalUIComponent])
+ assert_type(row_msg, ActionRow[MessageUIComponent])
def test_rows_from_message(self) -> None:
rows = [
diff --git a/tests/ui/test_decorators.py b/tests/ui/test_decorators.py
index 86ecd65ba9..e9c3680873 100644
--- a/tests/ui/test_decorators.py
+++ b/tests/ui/test_decorators.py
@@ -30,21 +30,21 @@ def __init__(self, *, param: float = 42.0) -> None:
class TestDecorator:
def test_default(self) -> None:
- with create_callback(ui.Button) as func:
+ with create_callback(ui.Button[ui.View]) as func:
res = ui.button(custom_id="123")(func)
- assert_type(res, ui.item.DecoratedItem[ui.Button])
+ assert_type(res, ui.item.DecoratedItem[ui.Button[ui.View]])
assert func.__discord_ui_model_type__ is ui.Button
assert func.__discord_ui_model_kwargs__ == {"custom_id": "123"}
- with create_callback(ui.StringSelect) as func:
+ with create_callback(ui.StringSelect[ui.View]) as func:
res = ui.string_select(custom_id="123")(func)
- assert_type(res, ui.item.DecoratedItem[ui.StringSelect])
+ assert_type(res, ui.item.DecoratedItem[ui.StringSelect[ui.View]])
assert func.__discord_ui_model_type__ is ui.StringSelect
assert func.__discord_ui_model_kwargs__ == {"custom_id": "123"}
- # from here on out we're only testing the button decorator,
+ # from here on out we're mostly only testing the button decorator,
# as @ui.string_select etc. works identically
@pytest.mark.parametrize("cls", [_CustomButton, _CustomButton[Any]])
@@ -64,7 +64,18 @@ def _test_typing_cls(self) -> None:
this_should_not_work="h", # type: ignore
)
- @pytest.mark.parametrize("cls", [123, int, ui.StringSelect])
- def test_cls_invalid(self, cls) -> None:
- with pytest.raises(TypeError, match=r"cls argument must be"):
- ui.button(cls=cls) # type: ignore
+ @pytest.mark.parametrize(
+ ("decorator", "invalid_cls"),
+ [
+ (ui.button, ui.StringSelect),
+ (ui.string_select, ui.Button),
+ (ui.user_select, ui.Button),
+ (ui.role_select, ui.Button),
+ (ui.mentionable_select, ui.Button),
+ (ui.channel_select, ui.Button),
+ ],
+ )
+ def test_cls_invalid(self, decorator, invalid_cls) -> None:
+ for cls in [123, int, invalid_cls]:
+ with pytest.raises(TypeError, match=r"cls argument must be"):
+ decorator(cls=cls)
diff --git a/tests/utils_helper_module.py b/tests/utils_helper_module.py
new file mode 100644
index 0000000000..7711e861b8
--- /dev/null
+++ b/tests/utils_helper_module.py
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: MIT
+
+"""Separate module file for some test_utils.py type annotation tests."""
+
+import sys
+from typing import TYPE_CHECKING, List, TypeVar, Union
+
+version = sys.version_info # assign to variable to trick pyright
+
+if TYPE_CHECKING:
+ from typing_extensions import TypeAliasType
+elif version >= (3, 12):
+ # non-3.12 tests shouldn't be using this
+ from typing import TypeAliasType
+
+if version >= (3, 12):
+ CoolUniqueIntOrStrAlias = Union[int, str]
+ ListWithForwardRefAlias = TypeAliasType(
+ "ListWithForwardRefAlias", List["CoolUniqueIntOrStrAlias"]
+ )
+
+ T = TypeVar("T")
+ GenericListAlias = TypeAliasType("GenericListAlias", List[T], type_params=(T,))
+
+ DuplicateAlias = str
+ ListWithDuplicateAlias = TypeAliasType("ListWithDuplicateAlias", List["DuplicateAlias"])