-
Notifications
You must be signed in to change notification settings - Fork 138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Typed IDs. #1149
Comments
This is definitely an interesting proposal, thanks!
I can't think of situations where a mistake like that could cause any damage, given that IDs are unique (barring a few exceptions), but it's rather annoying and difficult to debug nonetheless, yeah.
Sounds reasonable.
Compatibility is my main concern here, even if it's just in the type-checking realm, and not at runtime. The ideal compatible solution would be making methods accept something like I don't have any data on how often mix-ups between different ID "types" happen - I imagine it's probably not that frequent to warrant a fundamental change like this, personally. |
I think we can make an exception for those and |
Wouldn't the identity function overhead be dwarfed by the int cast anyway? |
This is cool and all, but if they were to do this it would bring some really weird code. For example: everytime you want to search someone up you would have to |
And you can make an error_handler using disnake.ext.commands.UserNotFound for example. |
This proposal's intention is to allow optional, type-check-time guarantees about validness of ID usage. Whether to use it or not is up to you, and if you prefer catching errors at runtime instead, you will be able to do that. (if we find a solution for the compatibility problem..) |
There could be something like |
The union would defeat the purpose of this issue, since |
|
It is not a subclass. At runtime it behaves like identity function. |
An idea could be to use what PEP-702 propose, allowing for from typing import overload
from warnings import deprecated
@overload
async def fetch_member(self, id: UserId) -> disnake.Member:
...
@overload
@deprecated("Only UserId will be supported in future versions")
async def fetch_member(self, id: int) -> disnake.Member:
...
async def fetch_member(self, id: Union[UserId, int], /) -> disnake.Member:
... this will return a deprecation warning message by the type checker if the user invoke the method with an |
@Snipy7374 This is an interesting idea, thanks! Sadly it still has the same problem about |
As far as the overloads go, I think we could make a decorator for this. |
I've written some example implementation of how the forementioned decorator could look like. from collections import abc
from typing import Concatenate, Never, NewType, ParamSpec, Protocol, overload
from typing_extensions import TypeVar, deprecated
ObjectID = NewType("ObjectID", int)
IdT = TypeVar("IdT", bound=ObjectID, infer_variance=True)
P = ParamSpec("P")
RetT = TypeVar("RetT", infer_variance=True)
class AcceptsID(Protocol[IdT, P, RetT]):
@overload
def __call__(self, id: IdT, /, *args: P.args, **kwargs: P.kwargs) -> RetT: ...
@overload
def __call__(self, id: ObjectID, /, *args: P.args, **kwargs: P.kwargs) -> Never: ...
@overload
@deprecated("Using raw ints is not value-safe", category=None)
def __call__(self, id: int, /, *args: P.args, **kwargs: P.kwargs) -> RetT: ...
def overload_id(func: abc.Callable[Concatenate[IdT | int, P], RetT], /) -> AcceptsID[IdT, P, RetT]:
return func # pyright: ignore[reportReturnType] I've added the intermediary AppID = NewType("AppID", ObjectID)
ChannelID = NewType("ChannelID", ObjectID)
@overload_id
def get_channel(id: ChannelID | int, /) -> object: ... Passing in an int shows the depreciation message. Passing in |
I took a bite at implementing this. # Client.py
# ObjectId is a Union of all IDs
@overload
def get_channel(self, id: ChannelId, /) -> Optional[GuildChannel]: ...
@overload
def get_channel(self, id: ThreadId, /) -> Optional[Thread]: ...
@overload
def get_channel(self, id: PrivateChannelId, /) -> Optional[PrivateChannel]: ...
@overload
def get_channel(self, id: ObjectId, /) -> None: ... # the "wrong ID" case
@overload
def get_channel(self, id: int, /) -> Optional[Union[GuildChannel, Thread, PrivateChannel]]: ...
def get_channel(self, id: Union[ObjectId, int], /) -> Optional[Union[GuildChannel, Thread, PrivateChannel]]: Though obviously someone has to write them 😐 |
See Enegg@e325f87 |
Summary
Change all ID types from
int
to*Id
newtypes for greater type safety.What is the feature request for?
The core library
The Problem
Currently, this (~pseudo) code passes type checks, but in reality this will almost certainly raise an exception:
While the above code is obviously wrong, command bodies can get large enough in practice that the error will not be as easy to spot. Using IDs in incorrect places usually results in an unhandled exception, but in very extreme cases can lead to damage (f.e. as thread ids are the same as the original message ids, one can delete thread instead of the original message or vice versa). The more developer passes IDs around, the more is the chance of making mistakes like this.
The Ideal Solution
Change all raw
int
IDs to appropriate newtypes. This incurs zero (or close to zero) overhead, but allows to prevent incorrect usage of IDs.The implementation is very straightforward:
The rest is only about updating the library to use
Id
s instead of rawint
s, and maybe updating converters.The Current Solution
Carefully reviewing, testing or otherwise ensuring that IDs for e.g. channels are not used where e.g. a role Id was expected.
Additional Context
I am willing to work on this.Not anymore :(This was originally inspired by twilight-model's
Id
.Open Questions
*Id
is expected?The text was updated successfully, but these errors were encountered: