From 038a365a16782951f6ebbaee32b47b94c98c4925 Mon Sep 17 00:00:00 2001
From: shiftinv <8530778+shiftinv@users.noreply.github.com>
Date: Sun, 29 Oct 2023 13:00:20 +0100
Subject: [PATCH 1/6] fix(ui): don't require `cls` argument in select
decorators to be positional (#1111)
---
changelog/1111.bugfix.rst | 1 +
disnake/ui/button.py | 2 +-
disnake/ui/select/channel.py | 6 ++----
disnake/ui/select/mentionable.py | 6 ++----
disnake/ui/select/role.py | 6 ++----
disnake/ui/select/string.py | 6 ++----
disnake/ui/select/user.py | 6 ++----
docs/api/ui.rst | 2 +-
tests/ui/test_decorators.py | 21 ++++++++++++++++-----
9 files changed, 29 insertions(+), 27 deletions(-)
create mode 100644 changelog/1111.bugfix.rst
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/disnake/ui/button.py b/disnake/ui/button.py
index 8bb1e60ff2..d5e1fc7708 100644
--- a/disnake/ui/button.py
+++ b/disnake/ui/button.py
@@ -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/select/channel.py b/disnake/ui/select/channel.py
index a455172799..a98472b547 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, P]] = 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..4f0d591201 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, P]] = 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..69b1bcaa57 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, P]] = 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..d38c9ea6ba 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, P]] = 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..179b9d6c74 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, P]] = 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/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/tests/ui/test_decorators.py b/tests/ui/test_decorators.py
index 86ecd65ba9..5fab1bb787 100644
--- a/tests/ui/test_decorators.py
+++ b/tests/ui/test_decorators.py
@@ -44,7 +44,7 @@ def test_default(self) -> None:
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)
From 627e9e90b99ca673428a2382dfb4f5549d7cf64e Mon Sep 17 00:00:00 2001
From: shiftinv <8530778+shiftinv@users.noreply.github.com>
Date: Mon, 30 Oct 2023 20:27:45 +0100
Subject: [PATCH 2/6] feat(ci): automated release workflow (#1072)
---
.github/CODEOWNERS | 3 +
.github/workflows/build-release.yaml | 227 +++++++++++++++++++++++
.github/workflows/changelog.yaml | 2 +-
.github/workflows/create-release-pr.yaml | 81 ++++++++
.github/workflows/lint-test.yml | 9 +-
.pre-commit-config.yaml | 2 +-
RELEASE.md | 47 +++++
disnake/__init__.py | 2 +
pyproject.toml | 12 +-
scripts/ci/versiontool.py | 128 +++++++++++++
10 files changed, 505 insertions(+), 8 deletions(-)
create mode 100644 .github/CODEOWNERS
create mode 100644 .github/workflows/build-release.yaml
create mode 100644 .github/workflows/create-release-pr.yaml
create mode 100644 RELEASE.md
create mode 100644 scripts/ci/versiontool.py
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 696bdc5fb0..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:
@@ -124,10 +124,11 @@ 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
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0c599e6c6d..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
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/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/pyproject.toml b/pyproject.toml
index 984bdf767f..4756d55c4a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -70,10 +70,12 @@ 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.292",
]
+changelog = [
+ "towncrier==23.6.0",
+]
codemod = [
# run codemods on the respository (mostly automated typing)
"libcst~=1.1.0",
@@ -94,6 +96,11 @@ test = [
"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" }
@@ -230,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
@@ -378,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()
From 25d1d7a3f5dc09f811a8737613ef3bef7fba38d9 Mon Sep 17 00:00:00 2001
From: shiftinv <8530778+shiftinv@users.noreply.github.com>
Date: Fri, 3 Nov 2023 21:08:42 +0100
Subject: [PATCH 3/6] docs: add v2.9.1 changelog to latest docs (#1131)
---
docs/whats_new.rst | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
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
From 59b101f923c193a2f58b7a844eae47a3fb7869c0 Mon Sep 17 00:00:00 2001
From: lena <77104725+elenakrittik@users.noreply.github.com>
Date: Sat, 4 Nov 2023 21:59:16 +0300
Subject: [PATCH 4/6] fix(commands): fix application command checks' typing
(#1048)
Signed-off-by: lena <77104725+elenakrittik@users.noreply.github.com>
Co-authored-by: shiftinv <8530778+shiftinv@users.noreply.github.com>
---
changelog/1045.bugfix.rst | 1 +
changelog/1045.feature.rst | 1 +
disnake/ext/commands/_types.py | 6 +++
disnake/ext/commands/base_core.py | 10 ++--
disnake/ext/commands/cog.py | 24 +++++-----
disnake/ext/commands/core.py | 50 +++++++++++++++++++-
disnake/ext/commands/errors.py | 15 +++---
disnake/ext/commands/interaction_bot_base.py | 14 +++---
docs/ext/commands/api/checks.rst | 6 +++
9 files changed, 96 insertions(+), 31 deletions(-)
create mode 100644 changelog/1045.bugfix.rst
create mode 100644 changelog/1045.feature.rst
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/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..7198394be8 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
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/core.py b/disnake/ext/commands/core.py
index 669dd60e04..2bb108e966 100644
--- a/disnake/ext/commands/core.py
+++ b/disnake/ext/commands/core.py
@@ -67,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__ = (
@@ -81,6 +81,8 @@
"has_any_role",
"check",
"check_any",
+ "app_check",
+ "app_check_any",
"before_invoke",
"after_invoke",
"bot_has_role",
@@ -1695,6 +1697,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.
@@ -1767,6 +1772,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
@@ -1823,6 +1831,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.
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/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/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:
From 2c85e39f415e9d96867dc317592582b685888ed9 Mon Sep 17 00:00:00 2001
From: shiftinv <8530778+shiftinv@users.noreply.github.com>
Date: Thu, 16 Nov 2023 15:37:08 +0100
Subject: [PATCH 5/6] feat(commands): don't parse self/ctx parameter
annotations of prefix command callbacks (#847)
---
changelog/847.feature.rst | 1 +
disnake/ext/commands/core.py | 40 +----------
disnake/ext/commands/help.py | 10 ---
disnake/ext/commands/params.py | 30 +++++---
disnake/utils.py | 70 ++++++++++++++++++-
tests/ext/commands/test_params.py | 110 +++++++++++++++++-------------
tests/test_utils.py | 82 ++++++++++++++++++++++
7 files changed, 235 insertions(+), 108 deletions(-)
create mode 100644 changelog/847.feature.rst
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/ext/commands/core.py b/disnake/ext/commands/core.py
index 2bb108e966..fda34b5a95 100644
--- a/disnake/ext/commands/core.py
+++ b/disnake/ext/commands/core.py
@@ -381,7 +381,7 @@ def callback(self, function: CommandCallback[CogT, Any, P, T]) -> None:
except AttributeError:
globalns = {}
- 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.")
@@ -607,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:
@@ -693,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)
diff --git a/disnake/ext/commands/help.py b/disnake/ext/commands/help.py
index 5841a8ba11..483d4f4bd2 100644
--- a/disnake/ext/commands/help.py
+++ b/disnake/ext/commands/help.py
@@ -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
diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py
index 5aae2de611..95679ed802 100644
--- a/disnake/ext/commands/params.py
+++ b/disnake/ext/commands/params.py
@@ -42,7 +42,12 @@
from disnake.ext import commands
from disnake.i18n import Localized
from disnake.interactions import ApplicationCommandInteraction
-from disnake.utils import get_signature_parameters, get_signature_return, 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
@@ -771,7 +776,7 @@ def parse_converter_annotation(self, converter: Callable, fallback_annotation: A
# (we need `__call__` here to get the correct global namespace later, since
# classes do not have `__globals__`)
converter_func = converter.__call__
- _, parameters = isolate_self(get_signature_parameters(converter_func))
+ _, parameters = isolate_self(converter_func)
if len(parameters) != 1:
raise TypeError(
@@ -879,9 +884,16 @@ def safe_call(function: Callable[..., T], /, *possible_args: Any, **possible_kwa
def isolate_self(
- parameters: Dict[str, inspect.Parameter],
+ 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"""
+ """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), {}
@@ -891,7 +903,7 @@ def isolate_self(
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:
@@ -941,15 +953,11 @@ def collect_params(
) -> Tuple[Optional[str], Optional[str], List[ParamInfo], Dict[str, Injection]]:
"""Collect all parameters in a function.
- Optionally accepts a `{str: inspect.Parameter}` dict as an optimization,
- calls `get_signature_parameters(function)` if not provided.
+ Optionally accepts a `{str: inspect.Parameter}` dict as an optimization.
Returns: (`cog parameter`, `interaction parameter`, `param infos`, `injections`)
"""
- if parameters is None:
- parameters = get_signature_parameters(function)
-
- (cog_param, inter_param), parameters = isolate_self(parameters)
+ (cog_param, inter_param), parameters = isolate_self(function, parameters)
doc = disnake.utils.parse_docstring(function)["params"]
diff --git a/disnake/utils.py b/disnake/utils.py
index d40cd4e8fe..a74d50ab94 100644
--- a/disnake/utils.py
+++ b/disnake/utils.py
@@ -12,6 +12,7 @@
import pkgutil
import re
import sys
+import types
import unicodedata
import warnings
from base64 import b64encode
@@ -1227,7 +1228,10 @@ def _get_function_globals(function: Callable[..., Any]) -> Dict[str, Any]:
def get_signature_parameters(
- function: Callable[..., Any], globalns: Optional[Dict[str, Any]] = None
+ 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:
@@ -1237,9 +1241,23 @@ def get_signature_parameters(
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 signature.parameters.items():
+ for name, parameter in iterator:
annotation = parameter.annotation
if annotation is _inspect_empty:
params[name] = parameter
@@ -1270,6 +1288,54 @@ def get_signature_return(function: Callable[..., Any]) -> Any:
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/tests/ext/commands/test_params.py b/tests/ext/commands/test_params.py
index 61c812c8f0..96f2c08c32 100644
--- a/tests/ext/commands/test_params.py
+++ b/tests/ext/commands/test_params.py
@@ -10,7 +10,6 @@
import disnake
from disnake import Member, Role, User
from disnake.ext import commands
-from disnake.ext.commands import params
OptionType = disnake.OptionType
@@ -67,53 +66,6 @@ async def test_verify_type__invalid_member(self, annotation, arg_types) -> None:
with pytest.raises(commands.errors.MemberNotFound):
await info.verify_type(mock.Mock(), arg_mock)
- def test_isolate_self(self) -> None:
- def func(a: int) -> None:
- ...
-
- (cog, inter), parameters = params.isolate_self(params.get_signature_parameters(func))
- assert cog is None
- assert inter is None
- assert parameters == ({"a": mock.ANY})
-
- def test_isolate_self_inter(self) -> None:
- def func(i: disnake.ApplicationCommandInteraction, a: int) -> None:
- ...
-
- (cog, inter), parameters = params.isolate_self(params.get_signature_parameters(func))
- assert cog is None
- assert inter is not None
- assert parameters == ({"a": mock.ANY})
-
- def test_isolate_self_cog_inter(self) -> None:
- def func(self, i: disnake.ApplicationCommandInteraction, a: int) -> None:
- ...
-
- (cog, inter), parameters = params.isolate_self(params.get_signature_parameters(func))
- assert cog is not None
- assert inter is not None
- assert parameters == ({"a": mock.ANY})
-
- def test_isolate_self_generic(self) -> None:
- def func(i: disnake.ApplicationCommandInteraction[commands.Bot], a: int) -> None:
- ...
-
- (cog, inter), parameters = params.isolate_self(params.get_signature_parameters(func))
- assert cog is None
- assert inter is not None
- assert parameters == ({"a": mock.ANY})
-
- def test_isolate_self_union(self) -> None:
- def func(
- i: Union[commands.Context, disnake.ApplicationCommandInteraction[commands.Bot]], a: int
- ) -> None:
- ...
-
- (cog, inter), parameters = params.isolate_self(params.get_signature_parameters(func))
- assert cog is None
- assert inter is not None
- assert parameters == ({"a": mock.ANY})
-
# this uses `Range` for testing `_BaseRange`, `String` should work equally
class TestBaseRange:
@@ -260,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/test_utils.py b/tests/test_utils.py
index a8f52e6b1f..d767264a95 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -2,6 +2,7 @@
import asyncio
import datetime
+import functools
import inspect
import os
import sys
@@ -880,3 +881,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
From 4bd0d25ac787adb405d270ad6236245f82a1fd07 Mon Sep 17 00:00:00 2001
From: shiftinv <8530778+shiftinv@users.noreply.github.com>
Date: Fri, 17 Nov 2023 20:09:17 +0100
Subject: [PATCH 6/6] build(deps): update pyright to v1.1.336 (#1122)
---
disnake/abc.py | 2 +-
disnake/activity.py | 2 +-
disnake/asset.py | 2 +-
disnake/audit_logs.py | 2 +-
disnake/channel.py | 12 ++---
disnake/client.py | 32 ++++++++---
disnake/components.py | 48 +++++++++++------
disnake/emoji.py | 2 +-
disnake/enums.py | 4 +-
disnake/ext/commands/base_core.py | 2 +-
disnake/ext/commands/bot_base.py | 17 ++----
disnake/ext/commands/converter.py | 6 +--
disnake/ext/commands/cooldowns.py | 2 +-
disnake/ext/commands/core.py | 4 +-
disnake/ext/commands/help.py | 6 ++-
disnake/ext/commands/params.py | 7 +--
disnake/ext/commands/slash_core.py | 4 +-
disnake/ext/tasks/__init__.py | 4 +-
disnake/gateway.py | 2 +-
disnake/guild.py | 4 --
disnake/http.py | 25 ++++-----
disnake/i18n.py | 2 +-
disnake/interactions/base.py | 2 +-
disnake/iterators.py | 10 ++--
disnake/message.py | 7 +--
disnake/shard.py | 3 +-
disnake/state.py | 8 ++-
disnake/types/audit_log.py | 84 ++++++++++++++---------------
disnake/types/automod.py | 4 +-
disnake/types/template.py | 2 +-
disnake/ui/action_row.py | 5 +-
disnake/ui/button.py | 2 +-
disnake/ui/item.py | 2 +-
disnake/ui/modal.py | 2 +-
disnake/ui/select/channel.py | 2 +-
disnake/ui/select/mentionable.py | 2 +-
disnake/ui/select/role.py | 2 +-
disnake/ui/select/string.py | 2 +-
disnake/ui/select/user.py | 2 +-
disnake/utils.py | 12 +++--
disnake/voice_client.py | 2 +-
docs/extensions/builder.py | 2 +-
examples/basic_voice.py | 4 +-
examples/interactions/injections.py | 2 +-
examples/interactions/modal.py | 2 +
pyproject.toml | 2 +-
test_bot/cogs/modals.py | 2 +-
tests/ui/test_decorators.py | 8 +--
48 files changed, 193 insertions(+), 175 deletions(-)
diff --git a/disnake/abc.py b/disnake/abc.py
index 605bb725aa..b9a60f3ee5 100644
--- a/disnake/abc.py
+++ b/disnake/abc.py
@@ -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):
diff --git a/disnake/activity.py b/disnake/activity.py
index 92460cd35d..3c290edd17 100644
--- a/disnake/activity.py
+++ b/disnake/activity.py
@@ -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/asset.py b/disnake/asset.py
index fad72c79ce..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"}
diff --git a/disnake/audit_logs.py b/disnake/audit_logs.py
index e8ab022edf..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
diff --git a/disnake/channel.py b/disnake/channel.py
index 7eef52b942..ffb11f2d2c 100644
--- a/disnake/channel.py
+++ b/disnake/channel.py
@@ -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
@@ -1628,7 +1628,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
@@ -2453,7 +2453,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
@@ -2946,7 +2946,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
@@ -3619,7 +3619,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
@@ -3994,7 +3994,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)
)
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/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/emoji.py b/disnake/emoji.py
index 0f3d02c27d..2a24877b07 100644
--- a/disnake/emoji.py
+++ b/disnake/emoji.py
@@ -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)]
diff --git a/disnake/enums.py b/disnake/enums.py
index b4bf3d994d..cb603c5425 100644
--- a/disnake/enums.py
+++ b/disnake/enums.py
@@ -466,7 +466,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"
@@ -627,7 +627,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
diff --git a/disnake/ext/commands/base_core.py b/disnake/ext/commands/base_core.py
index 7198394be8..3599ea0908 100644
--- a/disnake/ext/commands/base_core.py
+++ b/disnake/ext/commands/base_core.py
@@ -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
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/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 fda34b5a95..2ddcb10075 100644
--- a/disnake/ext/commands/core.py
+++ b/disnake/ext/commands/core.py
@@ -755,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
@@ -1718,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
diff --git a/disnake/ext/commands/help.py b/disnake/ext/commands/help.py
index 483d4f4bd2..ecd3988b86 100644
--- a/disnake/ext/commands/help.py
+++ b/disnake/ext/commands/help.py
@@ -368,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/params.py b/disnake/ext/commands/params.py
index 95679ed802..9114b8b353 100644
--- a/disnake/ext/commands/params.py
+++ b/disnake/ext/commands/params.py
@@ -85,9 +85,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)
@@ -543,7 +540,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)
@@ -1339,7 +1336,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):
diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py
index 1b318a21d0..4652c552f8 100644
--- a/disnake/ext/commands/slash_core.py
+++ b/disnake/ext/commands/slash_core.py
@@ -666,7 +666,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 +695,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/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 3927992fb5..ba140f2298 100644
--- a/disnake/guild.py
+++ b/disnake/guild.py
@@ -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]
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..c2781a9eb8 100644
--- a/disnake/i18n.py
+++ b/disnake/i18n.py
@@ -409,7 +409,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..01637be96a 100644
--- a/disnake/interactions/base.py
+++ b/disnake/interactions/base.py
@@ -1855,7 +1855,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/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/message.py b/disnake/message.py
index 92aba532c7..e3967e1160 100644
--- a/disnake/message.py
+++ b/disnake/message.py
@@ -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
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/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/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/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 fe7244a776..21ea01cb74 100644
--- a/disnake/ui/action_row.py
+++ b/disnake/ui/action_row.py
@@ -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 d5e1fc7708..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.
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 a98472b547..57dd9cfbe9 100644
--- a/disnake/ui/select/channel.py
+++ b/disnake/ui/select/channel.py
@@ -168,7 +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.
diff --git a/disnake/ui/select/mentionable.py b/disnake/ui/select/mentionable.py
index 4f0d591201..860903f7f1 100644
--- a/disnake/ui/select/mentionable.py
+++ b/disnake/ui/select/mentionable.py
@@ -144,7 +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.
diff --git a/disnake/ui/select/role.py b/disnake/ui/select/role.py
index 69b1bcaa57..fe2da2f97a 100644
--- a/disnake/ui/select/role.py
+++ b/disnake/ui/select/role.py
@@ -142,7 +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.
diff --git a/disnake/ui/select/string.py b/disnake/ui/select/string.py
index d38c9ea6ba..3eeedc1f22 100644
--- a/disnake/ui/select/string.py
+++ b/disnake/ui/select/string.py
@@ -268,7 +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.
diff --git a/disnake/ui/select/user.py b/disnake/ui/select/user.py
index 179b9d6c74..4868894a83 100644
--- a/disnake/ui/select/user.py
+++ b/disnake/ui/select/user.py
@@ -143,7 +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.
diff --git a/disnake/utils.py b/disnake/utils.py
index a74d50ab94..6fa6ae82d7 100644
--- a/disnake/utils.py
+++ b/disnake/utils.py
@@ -134,6 +134,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]):
@@ -489,7 +490,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"):
@@ -502,14 +503,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:
@@ -538,7 +539,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
@@ -571,7 +572,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
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/docs/extensions/builder.py b/docs/extensions/builder.py
index 5133af0f85..61e366d2ca 100644
--- a/docs/extensions/builder.py
+++ b/docs/extensions/builder.py
@@ -65,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/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/pyproject.toml b/pyproject.toml
index 4756d55c4a..73f3ad1b9f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -84,7 +84,7 @@ codemod = [
]
typing = [
# this is not pyright itself, but the python wrapper
- "pyright==1.1.291",
+ "pyright==1.1.336",
"typing-extensions~=4.8.0",
# only used for type-checking, version does not matter
"pytz",
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/ui/test_decorators.py b/tests/ui/test_decorators.py
index 5fab1bb787..e9c3680873 100644
--- a/tests/ui/test_decorators.py
+++ b/tests/ui/test_decorators.py
@@ -30,16 +30,16 @@ 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"}