diff --git a/.github/workflows/docs-checks.yml b/.github/workflows/docs-checks.yml index 85bfc8267d..65b1b68338 100644 --- a/.github/workflows/docs-checks.yml +++ b/.github/workflows/docs-checks.yml @@ -8,7 +8,7 @@ on: - "requirements/**" - "*.toml" - "*.py" - branches: [master] + branches: [ master ] pull_request: paths: - "discord/**" @@ -40,7 +40,7 @@ jobs: - name: "Setup Python" uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "requirements/docs.txt" check-latest: true diff --git a/.github/workflows/docs-json-export.yml b/.github/workflows/docs-json-export.yml index d9e5904273..f4f1f8d5a7 100644 --- a/.github/workflows/docs-json-export.yml +++ b/.github/workflows/docs-json-export.yml @@ -18,7 +18,7 @@ jobs: uses: actions/setup-python@v6 id: setup-python with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "requirements/docs.txt" check-latest: true @@ -45,7 +45,7 @@ jobs: run: | head -n 40 docs.json || tail -n 40 docs.json - name: Output artifact ID - run: | + run: | echo "artifact-id=${{ steps.artifact-upload.outputs.artifact-id }}" >> $GITHUB_OUTPUT echo "artifact-url=${{ steps.artifact-upload.outputs.artifact-url }}" >> $GITHUB_OUTPUT echo "::notice::Artifact uploaded: ${{ steps.artifact-upload.outputs.artifact-url }}" diff --git a/.github/workflows/docs-localization-download.yml b/.github/workflows/docs-localization-download.yml index 54b44e69a2..2cf78f91bf 100644 --- a/.github/workflows/docs-localization-download.yml +++ b/.github/workflows/docs-localization-download.yml @@ -21,7 +21,7 @@ jobs: - name: "Install Python" uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "requirements/_locale.txt" - name: "Install Dependencies" @@ -69,7 +69,7 @@ jobs: pr: name: "PR operations" - needs: [download] + needs: [ download ] runs-on: ubuntu-latest environment: translations steps: diff --git a/.github/workflows/docs-localization-upload.yml b/.github/workflows/docs-localization-upload.yml index 169e853cc3..9117c19be2 100644 --- a/.github/workflows/docs-localization-upload.yml +++ b/.github/workflows/docs-localization-upload.yml @@ -5,11 +5,11 @@ on: paths: - "discord/**" - "docs/**" - branches: [master] + branches: [ master ] workflow_dispatch: schedule: - cron: "0 0 * * 1" - + permissions: write-all jobs: @@ -26,7 +26,7 @@ jobs: - name: "Install Python" uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "requirements/_locale.txt" - name: "Install Dependencies" diff --git a/.github/workflows/lib-checks.yml b/.github/workflows/lib-checks.yml index 0c622408bf..ea012693ee 100644 --- a/.github/workflows/lib-checks.yml +++ b/.github/workflows/lib-checks.yml @@ -36,7 +36,7 @@ jobs: - name: "Setup Python" uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "pyproject.toml" - name: "Install dependencies" @@ -56,7 +56,7 @@ jobs: - name: "Setup Python" uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "pyproject.toml" - name: "Install dependencies" @@ -74,7 +74,7 @@ jobs: - name: "Setup Python" uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "pyproject.toml" - name: "Install dependencies" @@ -98,7 +98,7 @@ jobs: - name: "Setup Python" uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "pyproject.toml" - name: "Install dependencies" @@ -124,7 +124,7 @@ jobs: - name: "Setup Python" uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "pyproject.toml" - name: "Install dependencies" @@ -141,7 +141,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] - python-version: [ "3.10", "3.11", "3.12", "3.13" ] + python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - name: "Checkout Repository" uses: actions/checkout@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02231fb948..b094ab904d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,7 +136,7 @@ jobs: id: python-setup uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: "pip" cache-dependency-path: "requirements/_release.txt" - name: "Install Release Dependencies" diff --git a/.readthedocs.yml b/.readthedocs.yml index ee1beb81d4..54236d5039 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,10 +1,10 @@ version: 2 -formats: [] +formats: [ ] build: os: ubuntu-22.04 tools: - python: "3.13" + python: "3.14" sphinx: configuration: docs/conf.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ea4d485762..45d6f3aaf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ These changes are available on the `master` branch, but have not yet been releas ### Added +- Support for **Python 3.14**. + ([#2948](https://github.com/Pycord-Development/pycord/pull/2948)) + ### Changed ### Fixed diff --git a/README.rst b/README.rst index fc7e038cb9..7ca0da1801 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ Pycord is a modern, easy to use, feature-rich, and async ready API wrapper for D Note ---- -Pycord supports Python ``3.10`` - ``3.13`` +Pycord supports Python ``3.10`` - ``3.14`` Key Features ------------ diff --git a/discord/client.py b/discord/client.py index 211532d68d..f227ccf1df 100644 --- a/discord/client.py +++ b/discord/client.py @@ -70,7 +70,13 @@ from .threads import Thread from .ui.view import BaseView from .user import ClientUser, User -from .utils import _D, _FETCHABLE, MISSING, warn_if_voice_dependencies_missing +from .utils import ( + _D, + _FETCHABLE, + MISSING, + _get_event_loop, + warn_if_voice_dependencies_missing, +) from .webhook import Webhook from .widget import Widget @@ -147,7 +153,7 @@ class Client: loop: Optional[:class:`asyncio.AbstractEventLoop`] The :class:`asyncio.AbstractEventLoop` to use for asynchronous operations. Defaults to ``None``, in which case the default event loop is used via - :func:`asyncio.get_event_loop()`. + :func:`asyncio.get_event_loop()` if it exists or one is created via :func:`asyncio.new_event_loop()`. connector: Optional[:class:`aiohttp.BaseConnector`] The connector to use for connection pooling. proxy: Optional[:class:`str`] @@ -245,7 +251,7 @@ def __init__( # self.ws is set in the connect method self.ws: DiscordWebSocket = None # type: ignore self.loop: asyncio.AbstractEventLoop = ( - asyncio.get_event_loop() if loop is None else loop + _get_event_loop() if loop is None else loop ) self._listeners: dict[str, list[tuple[asyncio.Future, Callable[..., bool]]]] = ( {} diff --git a/discord/ext/commands/cooldowns.py b/discord/ext/commands/cooldowns.py index 6e58d37f7a..7504eed7c8 100644 --- a/discord/ext/commands/cooldowns.py +++ b/discord/ext/commands/cooldowns.py @@ -32,6 +32,7 @@ import discord.abc from discord.enums import Enum +from discord.utils import _get_event_loop from ...abc import PrivateChannel from .errors import MaxConcurrencyReached @@ -308,7 +309,7 @@ class _Semaphore: def __init__(self, number: int) -> None: self.value: int = number - self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() + self.loop: asyncio.AbstractEventLoop = _get_event_loop() self._waiters: Deque[asyncio.Future] = deque() def __repr__(self) -> str: diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py index 9bdde87f23..6bb719c67f 100644 --- a/discord/ext/tasks/__init__.py +++ b/discord/ext/tasks/__init__.py @@ -38,7 +38,7 @@ import discord from discord.backoff import ExponentialBackoff -from discord.utils import MISSING +from discord.utils import MISSING, _get_event_loop __all__ = ("loop",) @@ -384,7 +384,7 @@ def start(self, *args: Any, **kwargs: Any) -> asyncio.Task[None]: args = (self._injected, *args) if self.loop is MISSING: - self.loop = asyncio.get_event_loop() + self.loop = _get_event_loop() self._task = self.loop.create_task(self._loop(*args, **kwargs)) return self._task @@ -825,8 +825,8 @@ def loop( using an exponential back-off algorithm similar to the one used in :meth:`discord.Client.connect`. loop: :class:`asyncio.AbstractEventLoop` - The loop to use to register the task, if not given - defaults to :func:`asyncio.get_event_loop`. + The loop to use to register the task, if not given the default event loop is used via + :func:`asyncio.get_event_loop()` if it exists or one is created via :func:`asyncio.new_event_loop()`. overlap: Union[:class:`bool`, :class:`int`] Controls whether overlapping executions of the task loop are allowed. Set to False (default) to run iterations one at a time, True for unlimited overlap, or an int to cap the number of concurrent runs. diff --git a/discord/http.py b/discord/http.py index 0717feadf5..fc6ffb22c3 100644 --- a/discord/http.py +++ b/discord/http.py @@ -55,7 +55,7 @@ from .file import VoiceMessage from .gateway import DiscordClientWebSocketResponse from .soundboard import PartialSoundboardSound, SoundboardSound -from .utils import MISSING +from .utils import MISSING, _get_event_loop _log = logging.getLogger(__name__) @@ -192,7 +192,7 @@ def __init__( unsync_clock: bool = True, ) -> None: self.loop: asyncio.AbstractEventLoop = ( - asyncio.get_event_loop() if loop is None else loop + _get_event_loop() if loop is None else loop ) self.connector = connector self.__session: aiohttp.ClientSession = MISSING # filled in static_login diff --git a/discord/ui/modal.py b/discord/ui/modal.py index 6bb3a21d6a..67c68913f9 100644 --- a/discord/ui/modal.py +++ b/discord/ui/modal.py @@ -33,7 +33,7 @@ from typing import TYPE_CHECKING, Any, Iterator, TypeVar from ..enums import ComponentType -from ..utils import find +from ..utils import _get_event_loop, find from .core import ItemInterface from .input_text import InputText from .item import ModalItem @@ -91,7 +91,7 @@ def __init__( for item in children: self.add_item(item) self._title = title - self.loop = asyncio.get_event_loop() + self.loop = _get_event_loop() def __repr__(self) -> str: attrs = " ".join( diff --git a/discord/utils.py b/discord/utils.py index cc6d9d3b19..d2e382fc53 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -1665,3 +1665,21 @@ def warn_if_voice_dependencies_missing() -> None: deps, "is" if len(missing) == 1 else "are", ) + + +def _get_event_loop() -> asyncio.AbstractEventLoop: + """Get the current event loop, creating one if necessary. + + Returns + ------- + asyncio.AbstractEventLoop + The current event loop. + """ + if sys.version_info >= (3, 14): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + return asyncio.get_event_loop() diff --git a/pyproject.toml b/pyproject.toml index 2e88d5a096..475094ecfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ ] description = "A Python wrapper for the Discord API" readme = { content-type = "text/x-rst", file = "README.rst" } -requires-python = ">=3.10, <3.14" +requires-python = ">=3.10, <3.15" license = "MIT" license-files = ["LICENSE"] classifiers = [ @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Internet", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", @@ -84,7 +85,7 @@ test = [ [tool.setuptools_scm] [tool.black] -target-version = ['py310', 'py311', 'py312', 'py313'] +target-version = ['py310', 'py311', 'py312', 'py313', 'py314'] [tool.isort] profile = "black" @@ -125,7 +126,7 @@ asyncio_default_fixture_loop_scope = "function" [tool.tox] requires = ["tox>=4"] -env_list = ["3.13", "3.12", "3.11", "3.10", "3.13-novoice"] +env_list = ["3.14", "3.13", "3.12", "3.11", "3.10", "3.14-novoice"] [tool.tox.env_run_base] description = "run unit tests" @@ -133,7 +134,7 @@ commands = [["pytest", { replace = "posargs", default = ["tests"], extend = true dependency_groups = ["test"] extras = ["voice"] -[tool.tox.env."3.13-novoice"] +[tool.tox.env."3.14-novoice"] description = "run import and warning tests without the voice extra" commands = [[ "pytest", @@ -144,7 +145,8 @@ extras = [] # GitHub actions [tool.tox.gh.python] -"3.13" = ["3.13", "3.13-novoice"] +"3.14" = ["3.14", "3.14-novoice"] +"3.13" = ["3.13"] "3.12" = ["3.12"] "3.11" = ["3.11"] "3.10" = ["3.10"]