diff --git a/.github/actions/setup-env/action.yml b/.github/actions/setup-env/action.yml index 9ffaaf051b..e3638f1860 100644 --- a/.github/actions/setup-env/action.yml +++ b/.github/actions/setup-env/action.yml @@ -6,7 +6,7 @@ inputs: python-version: description: The python version to install required: false - default: '3.9' + default: '3.10' use-cached-uv-lock: description: Whether to download the uv lock cache. required: false diff --git a/.github/workflows/create-release-pr.yaml b/.github/workflows/create-release-pr.yaml index e27210afdb..bb4783b033 100644 --- a/.github/workflows/create-release-pr.yaml +++ b/.github/workflows/create-release-pr.yaml @@ -45,7 +45,7 @@ jobs: - name: Set up environment uses: ./.github/actions/setup-env with: - python-version: 3.9 + python-version: 3.10 - name: Install dependencies run: uv sync --no-default-groups --group changelog diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 825c2647ad..ea10ded597 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -25,7 +25,7 @@ jobs: id: setup uses: ./.github/actions/setup-env with: - python-version: 3.9 + python-version: 3.10 - name: Build package run: | diff --git a/.readthedocs.yml b/.readthedocs.yml index 1e48672de0..f20bba070c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,7 +6,7 @@ formats: build: os: ubuntu-24.04 tools: - python: "3.9" + python: "3.10" jobs: create_environment: - asdf plugin add uv diff --git a/README.md b/README.md index 2afc1836eb..322635995f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Key Features Installing ---------- -**Python 3.9 or higher is required.** +**Python 3.10 or higher is required.** To install the library without full voice support, you can just run the following command: diff --git a/changelog/1394.breaking.rst b/changelog/1394.breaking.rst index bab57b3203..c17d241041 100644 --- a/changelog/1394.breaking.rst +++ b/changelog/1394.breaking.rst @@ -1 +1 @@ -Python 3.8 support has been dropped. +Drop support for Python 3.8 as it reached end of life in October 2024 and no longer receives security updates. diff --git a/changelog/1427.breaking.rst b/changelog/1427.breaking.rst new file mode 100644 index 0000000000..57dca36c31 --- /dev/null +++ b/changelog/1427.breaking.rst @@ -0,0 +1 @@ +Drop support for Python 3.9 as it reached end of life in October 2025 and no longer receives security updates. diff --git a/disnake/ext/commands/core.py b/disnake/ext/commands/core.py index 880c2d4975..d97e05428c 100644 --- a/disnake/ext/commands/core.py +++ b/disnake/ext/commands/core.py @@ -398,8 +398,7 @@ def callback(self, function: CommandCallback[CogT, Any, P, T]) -> None: str(e) + ", please check all annotations are defined outside of TYPE_CHECKING blocks." ) - # todo: add name kw only argument once we use py310+ - raise NameError(msg) from None + raise NameError(msg, name=e.name) from None for param in params.values(): if param.annotation is Greedy: diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index 51d9a31f64..33000e10b4 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -10,11 +10,11 @@ import inspect import itertools import math -import sys import types from abc import ABC, abstractmethod from dataclasses import dataclass from enum import Enum, EnumMeta +from types import EllipsisType, UnionType from typing import ( TYPE_CHECKING, Any, @@ -87,16 +87,6 @@ P = TypeVar("P") -if sys.version_info >= (3, 10): - from types import EllipsisType, UnionType -elif TYPE_CHECKING: - EllipsisType = type(Ellipsis) - UnionType = NoReturn - -else: - UnionType = object() - EllipsisType = type(Ellipsis) - T = TypeVar("T", bound=Any) TypeT = TypeVar("TypeT", bound=Type[Any]) BotT = TypeVar("BotT", bound="disnake.Client", covariant=True) @@ -361,10 +351,8 @@ def __call__(self) -> NoReturn: raise NotImplementedError # support new union syntax for `Range[int, 1, 2] | None` - if sys.version_info >= (3, 10): - - def __or__(self, other): - return Union[self, other] + def __or__(self, other): + return Union[self, other] if TYPE_CHECKING: diff --git a/disnake/ext/commands/slash_core.py b/disnake/ext/commands/slash_core.py index 6b46102873..65a4432831 100644 --- a/disnake/ext/commands/slash_core.py +++ b/disnake/ext/commands/slash_core.py @@ -313,8 +313,7 @@ def __init__( str(e) + ", please check all annotations are defined outside of TYPE_CHECKING blocks." ) - # todo: add name kw only argument once we use py310+ - raise NameError(msg) from None + raise NameError(msg, name=e.name) from None self.docstring = utils.parse_docstring(func) desc_loc = Localized._cast(description, False) @@ -488,8 +487,7 @@ def __init__( str(e) + ", please check all annotations are defined outside of TYPE_CHECKING blocks." ) - # todo: add name kw only argument once we use py310+ - raise NameError(msg) from None + raise NameError(msg, name=e.name) from None self.docstring = utils.parse_docstring(func) desc_loc = Localized._cast(description, False) diff --git a/disnake/role.py b/disnake/role.py index 15ad2ad61d..47272791df 100644 --- a/disnake/role.py +++ b/disnake/role.py @@ -519,7 +519,7 @@ async def _move(self, position: int, reason: Optional[str]) -> None: roles.append(self.id) payload: List[RolePositionUpdate] = [ - {"id": z[0], "position": z[1]} for z in zip(roles, change_range) + {"id": z[0], "position": z[1]} for z in zip(roles, change_range, strict=False) ] await http.move_role_position(self.guild.id, payload, reason=reason) diff --git a/disnake/utils.py b/disnake/utils.py index 003f1f7ed0..5923ac0fa1 100644 --- a/disnake/utils.py +++ b/disnake/utils.py @@ -19,6 +19,7 @@ from bisect import bisect_left from inspect import getdoc as _getdoc, isawaitable as _isawaitable, signature as _signature from operator import attrgetter +from types import UnionType from typing import ( TYPE_CHECKING, Any, @@ -43,7 +44,6 @@ TypedDict, TypeVar, Union, - get_origin, overload, ) from urllib.parse import parse_qs, urlencode @@ -1146,24 +1146,6 @@ def as_chunks(iterator: _Iter[T], max_size: int) -> _Iter[List[T]]: return _chunk(iterator, max_size) -if sys.version_info >= (3, 10): - PY_310 = True - from types import UnionType -else: - PY_310 = False - UnionType = object() - - -def flatten_literal_params(parameters: Iterable[Any]) -> Tuple[Any, ...]: - params = [] - for p in parameters: - if get_origin(p) is Literal: - params.extend(_unique(flatten_literal_params(p.__args__))) - else: - params.append(p) - return tuple(params) - - def normalise_optional_params(parameters: Iterable[Any]) -> Tuple[Any, ...]: none_cls = type(None) return (*tuple(p for p in parameters if p is not none_cls), none_cls) @@ -1237,8 +1219,6 @@ def evaluate_annotation( except ValueError: pass if origin is Literal: - if not PY_310: - args = flatten_literal_params(tp.__args__) implicit_str = False is_literal = True diff --git a/docs/intro.rst b/docs/intro.rst index 58cde9c70a..21dc64a61e 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -15,8 +15,8 @@ in creating applications that utilise the Discord API. Prerequisites ------------- -disnake works with Python 3.9 or higher. Support for earlier versions of Python -is not provided. Python 2.7 or lower is not supported. Python 3.8 or lower is not supported. +disnake works with Python 3.10 or higher. Support for earlier versions of Python +is not provided. Python 2.7 or lower is not supported. Python 3.9 or lower is not supported. .. _installing: diff --git a/noxfile.py b/noxfile.py index 0a90616d2a..40b38bbd25 100755 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,6 @@ #!/usr/bin/env -S uv run --script # /// script -# requires-python = ">=3.9" +# requires-python = ">=3.10" # dependencies = [ # "nox==2025.10.16", # ] diff --git a/pyproject.toml b/pyproject.toml index f42dcc121e..18bbad7254 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ readme = "README.md" authors = [ { name = "Disnake Development" } ] -requires-python = ">=3.9" +requires-python = ">=3.10" keywords = ["disnake", "discord", "discord api"] license = "MIT" license-files = ["LICENSE"] @@ -25,7 +25,6 @@ classifiers = [ "Natural Language :: English", "Operating System :: OS Independent", # it is important that the python classifiers are sorted in the sequential order - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -49,7 +48,6 @@ speed = [ # taken from aiohttp[speedups] "aiodns>=1.1", "Brotli", - 'cchardet; python_version < "3.10"', ] voice = [ "PyNaCl>=1.5.0,<1.6", @@ -214,6 +212,9 @@ ignore = [ "UP006", "UP035", "FURB188", + # py 3.10 bump + "UP007", + "UP045", ] [tool.ruff.lint.per-file-ignores] diff --git a/tests/ext/commands/test_params.py b/tests/ext/commands/test_params.py index 0f811a7204..ea6457c2a3 100644 --- a/tests/ext/commands/test_params.py +++ b/tests/ext/commands/test_params.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: MIT import math -import sys from typing import Any, Optional, Union from unittest import mock @@ -194,12 +193,7 @@ def test_string(self) -> None: [ "Optional[commands.Range[int, 1, 2]]", # 3.10 union syntax - pytest.param( - "commands.Range[int, 1, 2] | None", - marks=pytest.mark.skipif( - sys.version_info < (3, 10), reason="syntax requires py3.10" - ), - ), + "commands.Range[int, 1, 2] | None", ], ) def test_optional(self, annotation_str) -> None: diff --git a/tests/test_colour.py b/tests/test_colour.py index 365edff619..7d485da761 100644 --- a/tests/test_colour.py +++ b/tests/test_colour.py @@ -53,7 +53,9 @@ def test_from_rgb(value: int, parts: Tuple[int, int, int]) -> None: def test_from_hsv(value: int, parts: Tuple[float, float, float]) -> None: expected = Colour(value) col = Colour.from_hsv(*parts) - assert all(math.isclose(a, b, abs_tol=1) for a, b in zip(expected.to_rgb(), col.to_rgb())) + assert all( + math.isclose(a, b, abs_tol=1) for a, b in zip(expected.to_rgb(), col.to_rgb(), strict=True) + ) def test_alias() -> None: diff --git a/tests/test_utils.py b/tests/test_utils.py index fcc1ed60e8..5e7147f9d3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -37,6 +37,8 @@ # non-3.12 tests shouldn't be using this from typing import TypeAliasType +NoneType = type(None) + def test_missing() -> None: assert utils.MISSING != utils.MISSING @@ -749,23 +751,6 @@ def test_as_chunks_size(max_size: int) -> None: utils.as_chunks(iter([]), max_size) -@pytest.mark.parametrize( - ("params", "expected"), - [ - ([], ()), - ([disnake.CommandInter, int, Optional[str]], (disnake.CommandInter, int, Optional[str])), - # check flattening + deduplication (both of these are done automatically in 3.9.1+) - ([float, Literal[1, 2, Literal[3, 4]], Literal["a", "bc"]], (float, 1, 2, 3, 4, "a", "bc")), # noqa: RUF041 - ([Literal[1, 1, 2, 3, 3]], (1, 2, 3)), - ], -) -def test_flatten_literal_params(params, expected) -> None: - assert utils.flatten_literal_params(params) == expected - - -NoneType = type(None) - - @pytest.mark.parametrize( ("params", "expected"), [([NoneType], (NoneType,)), ([NoneType, int, NoneType, float], (int, float, NoneType))], @@ -792,12 +777,7 @@ def test_normalise_optional_params(params, expected) -> None: ("bool", bool, True), ("Tuple[dict, List[Literal[42, 99]]]", Tuple[dict, List[Literal[42, 99]]], True), # 3.10 union syntax - pytest.param( - "int | float", - Union[int, float], - True, - marks=pytest.mark.skipif(sys.version_info < (3, 10), reason="syntax requires py3.10"), - ), + ("int | float", Union[int, float], True), ], ) def test_resolve_annotation(tp, expected, expected_cache) -> None: diff --git a/tests/ui/test_action_row.py b/tests/ui/test_action_row.py index 38884d7ac0..ee9ec1827a 100644 --- a/tests/ui/test_action_row.py +++ b/tests/ui/test_action_row.py @@ -160,7 +160,7 @@ def test_rows_from_message(self) -> None: assert len(result) == len(rows) # compare component types and IDs - for actual, expected in zip(result, rows): + for actual, expected in zip(result, rows, strict=True): assert [(type(c), c.custom_id) for c in actual] == [ (type(c), c.custom_id) for c in expected ] @@ -188,7 +188,7 @@ def test_walk_components(self) -> None: expected = [(row, component) for row in rows for component in row.children] for (act_row, act_cmp), (exp_row, exp_cmp) in zip( - ActionRow.walk_components(rows), expected + ActionRow.walk_components(rows), expected, strict=True ): # test mutation (rows) # (remove row below the one containing select1)