From 0f080ab15b147b21e5f78e8e84c120ce3b1a65b7 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:18:14 +0200 Subject: [PATCH 1/7] refactor flag_value to return Flags instance rather than Self on class access --- disnake/flags.py | 145 ++++++++++++-------------------------------- tests/test_flags.py | 4 +- 2 files changed, 41 insertions(+), 108 deletions(-) diff --git a/disnake/flags.py b/disnake/flags.py index 5491641c4c..9b7ae665c3 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -4,13 +4,12 @@ import functools import operator -from collections.abc import Iterator, Sequence +from collections.abc import Iterator, Mapping, Sequence from typing import ( TYPE_CHECKING, Any, Callable, ClassVar, - Generic, NoReturn, Optional, TypeVar, @@ -19,7 +18,7 @@ ) from .enums import UserFlags -from .utils import MISSING, _generated +from .utils import _generated, deprecated if TYPE_CHECKING: from typing_extensions import Self @@ -46,52 +45,21 @@ "InteractionContextTypes", ) -BF = TypeVar("BF", bound="BaseFlags") T = TypeVar("T", bound="BaseFlags") -class flag_value(Generic[T]): +class flag_value: def __init__(self, func: Callable[[Any], int]) -> None: - self.flag = func(None) + self.flag: int = func(None) self.__doc__ = func.__doc__ - self._parent: type[T] = MISSING - - def __eq__(self, other: Any) -> bool: - if isinstance(other, flag_value): - return self.flag == other.flag - if isinstance(other, BaseFlags): - return self._parent is other.__class__ and self.flag == other.value - return False - - def __ne__(self, other: Any) -> bool: - return not self.__eq__(other) - - def __or__(self, other: Union[flag_value[T], T]) -> T: - if isinstance(other, BaseFlags): - if self._parent is not other.__class__: - msg = f"unsupported operand type(s) for |: flags of '{self._parent.__name__}' and flags of '{other.__class__.__name__}'" - raise TypeError(msg) - return other._from_value(self.flag | other.value) - if not isinstance(other, flag_value): - msg = f"unsupported operand type(s) for |: flags of '{self._parent.__name__}' and {other.__class__}" - raise TypeError(msg) - if self._parent is not other._parent: - msg = f"unsupported operand type(s) for |: flags of '{self._parent.__name__}' and flags of '{other._parent.__name__}'" - raise TypeError(msg) - return self._parent._from_value(self.flag | other.flag) - - def __invert__(self: flag_value[T]) -> T: - return ~self._parent._from_value(self.flag) @overload - def __get__(self, instance: None, owner: type[BF]) -> flag_value[BF]: ... - + def __get__(self, instance: None, owner: type[T]) -> T: ... @overload - def __get__(self, instance: BF, owner: type[BF]) -> bool: ... - - def __get__(self, instance: Optional[BF], owner: type[BF]) -> Any: + def __get__(self, instance: T, owner: type[T]) -> bool: ... + def __get__(self, instance: Optional[T], owner: type[T]) -> Union[bool, T]: if instance is None: - return self + return owner._from_value(self.flag) return instance._has_flag(self.flag) def __set__(self, instance: BaseFlags, value: bool) -> None: @@ -101,22 +69,27 @@ def __repr__(self) -> str: return f"" -class alias_flag_value(flag_value[T]): +class alias_flag_value(flag_value): pass -def all_flags_value(flags: dict[str, int]) -> int: +def all_flags_value(flags: Mapping[str, int]) -> int: return functools.reduce(operator.or_, flags.values()) class BaseFlags: - VALID_FLAGS: ClassVar[dict[str, int]] - DEFAULT_VALUE: ClassVar[int] + VALID_FLAGS: ClassVar[Mapping[str, int]] = {} + DEFAULT_VALUE: ClassVar[int] = 0 value: int __slots__ = ("value",) + @property + @deprecated("BaseFlags.value") + def flag(self) -> int: + return self.value + def __init__(self, **kwargs: bool) -> None: self.value = self.DEFAULT_VALUE for key, value in kwargs.items(): @@ -126,17 +99,16 @@ def __init__(self, **kwargs: bool) -> None: setattr(self, key, value) @classmethod - def __init_subclass__(cls, inverted: bool = False, no_fill_flags: bool = False) -> type[Self]: + def __init_subclass__(cls, inverted: bool = False, no_fill_flags: bool = False) -> None: # add a way to bypass filling flags, eg for ListBaseFlags. if no_fill_flags: - return cls + return - # use the parent's current flags as a base if they exist - cls.VALID_FLAGS = getattr(cls, "VALID_FLAGS", {}).copy() + # use a copy of the parent's current flags as a base if they exist + cls.VALID_FLAGS = dict(getattr(cls, "VALID_FLAGS", ())) for name, value in cls.__dict__.items(): if isinstance(value, flag_value): - value._parent = cls cls.VALID_FLAGS[name] = value.flag if not cls.VALID_FLAGS: @@ -145,107 +117,68 @@ def __init_subclass__(cls, inverted: bool = False, no_fill_flags: bool = False) cls.DEFAULT_VALUE = all_flags_value(cls.VALID_FLAGS) if inverted else 0 - return cls - @classmethod def _from_value(cls, value: int) -> Self: self = cls.__new__(cls) self.value = value return self - def __eq__(self, other: Any) -> bool: - if isinstance(other, self.__class__): - return self.value == other.value - if isinstance(other, flag_value): - return self.__class__ is other._parent and self.value == other.flag - return False - - def __ne__(self, other: Any) -> bool: - return not self.__eq__(other) + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return self.value == other.value def __and__(self, other: Self) -> Self: if not isinstance(other, self.__class__): - msg = f"unsupported operand type(s) for &: '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented return self._from_value(self.value & other.value) def __iand__(self, other: Self) -> Self: if not isinstance(other, self.__class__): - msg = f"unsupported operand type(s) for &=: '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented self.value &= other.value return self - def __or__(self, other: Union[Self, flag_value[Self]]) -> Self: - if isinstance(other, flag_value): - if self.__class__ is not other._parent: - msg = f"unsupported operand type(s) for |: flags of '{self.__class__.__name__}' and flags of '{other._parent.__name__}'" - raise TypeError(msg) - return self._from_value(self.value | other.flag) + def __or__(self, other: Self) -> Self: if not isinstance(other, self.__class__): - msg = f"unsupported operand type(s) for |: '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented return self._from_value(self.value | other.value) - def __ior__(self, other: Union[Self, flag_value[Self]]) -> Self: - if isinstance(other, flag_value): - if self.__class__ is not other._parent: - msg = f"unsupported operand type(s) for |=: flags of '{self.__class__.__name__}' and flags of '{other._parent.__name__}'" - raise TypeError(msg) - self.value |= other.flag - return self + def __ior__(self, other: Self) -> Self: if not isinstance(other, self.__class__): - msg = f"unsupported operand type(s) for |=: '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented self.value |= other.value return self - def __xor__(self, other: Union[Self, flag_value[Self]]) -> Self: - if isinstance(other, flag_value): - if self.__class__ is not other._parent: - msg = f"unsupported operand type(s) for ^: flags of '{self.__class__.__name__}' and flags of '{other._parent.__name__}'" - raise TypeError(msg) - return self._from_value(self.value ^ other.flag) + def __xor__(self, other: Self) -> Self: if not isinstance(other, self.__class__): - msg = f"unsupported operand type(s) for ^: '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented return self._from_value(self.value ^ other.value) - def __ixor__(self, other: Union[Self, flag_value[Self]]) -> Self: - if isinstance(other, flag_value): - if self.__class__ is not other._parent: - msg = f"unsupported operand type(s) for ^=: flags of '{self.__class__.__name__}' and flags of '{other._parent.__name__}'" - raise TypeError(msg) - self.value ^= other.flag - return self + def __ixor__(self, other: Self) -> Self: if not isinstance(other, self.__class__): - msg = f"unsupported operand type(s) for ^=: '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented self.value ^= other.value return self def __le__(self, other: Self) -> bool: if not isinstance(other, self.__class__): - msg = f"'<=' not supported between instances of '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented return (self.value & other.value) == self.value def __ge__(self, other: Self) -> bool: if not isinstance(other, self.__class__): - msg = f"'>=' not supported between instances of '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented return (self.value | other.value) == self.value def __lt__(self, other: Self) -> bool: if not isinstance(other, self.__class__): - msg = f"'<' not supported between instances of '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented return (self.value & other.value) == self.value and self.value != other.value def __gt__(self, other: Self) -> bool: if not isinstance(other, self.__class__): - msg = f"'>' not supported between instances of '{self.__class__.__name__}' and '{other.__class__.__name__}'" - raise TypeError(msg) + return NotImplemented return (self.value | other.value) == self.value and self.value != other.value def __invert__(self) -> Self: diff --git a/tests/test_flags.py b/tests/test_flags.py index 5deb7eb8cb..0b206a3eeb 100644 --- a/tests/test_flags.py +++ b/tests/test_flags.py @@ -111,7 +111,7 @@ def test_flag_value_or(self) -> None: assert ins.value == 5 assert (TestFlags.two | ins).value == 7 - assert not ins.value & TestFlags.sixteen.flag + assert not ins.value & TestFlags.sixteen.value ins |= TestFlags.sixteen assert ins.value == 21 @@ -421,7 +421,7 @@ def test_set_and_get_flag(self) -> None: ins.two = True assert ins.two is True - assert ins.value == TestFlags.two.flag == 1 << 1 + assert ins.value == TestFlags.two.value == 1 << 1 def test_alias_flag_value(self) -> None: ins = TestFlags(three=True) From 424907e90fcc6ce603e7cf38e9c91069798d8153 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:32:13 +0200 Subject: [PATCH 2/7] hide .flag behind TYPE_CHECKING --- disnake/flags.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/disnake/flags.py b/disnake/flags.py index 9b7ae665c3..99e905ec47 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -85,10 +85,11 @@ class BaseFlags: __slots__ = ("value",) - @property - @deprecated("BaseFlags.value") - def flag(self) -> int: - return self.value + if not TYPE_CHECKING: + @property + @deprecated("BaseFlags.value") + def flag(self) -> int: + return self.value def __init__(self, **kwargs: bool) -> None: self.value = self.DEFAULT_VALUE From 4ec3065ed19407560a143b28edc6698898ca662d Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:38:44 +0200 Subject: [PATCH 3/7] fix usages of .flag --- disnake/interactions/base.py | 4 +--- tests/test_permissions.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index 4a7012c430..ba71101491 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -934,9 +934,7 @@ async def defer( if defer_type is InteractionResponseType.deferred_channel_message: # we only want to set flags if we are sending a message - data["flags"] = 0 - if ephemeral: - data["flags"] |= MessageFlags.ephemeral.flag + data["flags"] = MessageFlags(ephemeral=ephemeral).value adapter = async_context.get() await adapter.create_interaction_response( diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 4350350805..05400f9b82 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -14,7 +14,7 @@ def test_init_permissions_keyword_arguments(self) -> None: assert perms.manage_messages is True # check we only have the manage message permission - assert perms.value == Permissions.manage_messages.flag + assert perms.value == Permissions.manage_messages.value def test_init_permissions_keyword_arguments_with_aliases(self) -> None: assert Permissions(read_messages=True, view_channel=False).value == 0 From 0c8cf0d6cba9a1e955bb97a6627667f24e20f06a Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:58:09 +0200 Subject: [PATCH 4/7] fix the fix --- disnake/flags.py | 2 +- disnake/interactions/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/flags.py b/disnake/flags.py index 99e905ec47..dff30a18cf 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -212,7 +212,7 @@ def _set_flag(self, o: int, toggle: bool) -> None: elif toggle is False: self.value &= ~o else: - msg = f"Value to set for {self.__class__.__name__} must be a bool." + msg = f"Value to set for {self.__class__.__name__} must be a bool, got {toggle!r}." raise TypeError(msg) diff --git a/disnake/interactions/base.py b/disnake/interactions/base.py index ba71101491..da219d54cc 100644 --- a/disnake/interactions/base.py +++ b/disnake/interactions/base.py @@ -934,7 +934,7 @@ async def defer( if defer_type is InteractionResponseType.deferred_channel_message: # we only want to set flags if we are sending a message - data["flags"] = MessageFlags(ephemeral=ephemeral).value + data["flags"] = MessageFlags(ephemeral=ephemeral is True).value adapter = async_context.get() await adapter.create_interaction_response( From 2435ff7ad2fbfd8c9a8e1c0ac45e56008d527dbd Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Tue, 21 Oct 2025 00:05:40 +0200 Subject: [PATCH 5/7] changelog --- changelog/1463.breaking.rst | 1 + changelog/1463.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 changelog/1463.breaking.rst create mode 100644 changelog/1463.feature.rst diff --git a/changelog/1463.breaking.rst b/changelog/1463.breaking.rst new file mode 100644 index 0000000000..679663a96c --- /dev/null +++ b/changelog/1463.breaking.rst @@ -0,0 +1 @@ +Class properites of :class:`BaseFlag` now return instances of :class:`BaseFlag` rather than :class:`flag_value`. If you are accessing :attr:`flag_value.flag`, change to :attr:`BaseFlag.value`. \ No newline at end of file diff --git a/changelog/1463.feature.rst b/changelog/1463.feature.rst new file mode 100644 index 0000000000..d3bcdd4947 --- /dev/null +++ b/changelog/1463.feature.rst @@ -0,0 +1 @@ +Class properites of :class:`BaseFlag` now return instances of :class:`BaseFlag`, allowing you to pass them directly where a flag instance is expected. \ No newline at end of file From 240bfbc9b3e50a8ba8a74563f8817e9e4a70dab9 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Tue, 21 Oct 2025 00:10:13 +0200 Subject: [PATCH 6/7] prek --- changelog/1463.breaking.rst | 2 +- changelog/1463.feature.rst | 2 +- disnake/flags.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog/1463.breaking.rst b/changelog/1463.breaking.rst index 679663a96c..230de8867f 100644 --- a/changelog/1463.breaking.rst +++ b/changelog/1463.breaking.rst @@ -1 +1 @@ -Class properites of :class:`BaseFlag` now return instances of :class:`BaseFlag` rather than :class:`flag_value`. If you are accessing :attr:`flag_value.flag`, change to :attr:`BaseFlag.value`. \ No newline at end of file +Class properties of :class:`BaseFlag` now return instances of :class:`BaseFlag` rather than :class:`flag_value`. If you are accessing :attr:`flag_value.flag`, change to :attr:`BaseFlag.value`. diff --git a/changelog/1463.feature.rst b/changelog/1463.feature.rst index d3bcdd4947..9b9daba3bf 100644 --- a/changelog/1463.feature.rst +++ b/changelog/1463.feature.rst @@ -1 +1 @@ -Class properites of :class:`BaseFlag` now return instances of :class:`BaseFlag`, allowing you to pass them directly where a flag instance is expected. \ No newline at end of file +Class properties of :class:`BaseFlag` now return instances of :class:`BaseFlag`, allowing you to pass them directly where a flag instance is expected. diff --git a/disnake/flags.py b/disnake/flags.py index dff30a18cf..6c4ec7c16e 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -86,6 +86,7 @@ class BaseFlags: __slots__ = ("value",) if not TYPE_CHECKING: + @property @deprecated("BaseFlags.value") def flag(self) -> int: From 9ba9e195ad17a462ca5e640f92375a9b8bc5b689 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:54:06 +0200 Subject: [PATCH 7/7] misc --- disnake/flags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/disnake/flags.py b/disnake/flags.py index 6c4ec7c16e..d9f37ebfb1 100644 --- a/disnake/flags.py +++ b/disnake/flags.py @@ -107,7 +107,7 @@ def __init_subclass__(cls, inverted: bool = False, no_fill_flags: bool = False) return # use a copy of the parent's current flags as a base if they exist - cls.VALID_FLAGS = dict(getattr(cls, "VALID_FLAGS", ())) + cls.VALID_FLAGS = dict(cls.VALID_FLAGS) for name, value in cls.__dict__.items(): if isinstance(value, flag_value): @@ -980,7 +980,7 @@ class Intents(BaseFlags): .. versionchanged:: 2.6 - This can be now be provided on initialisation. + This can be now be provided on initialization. """ __slots__ = ()