Skip to content

feat: Implement better get_or_fetch #2776

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

Open
wants to merge 89 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
dce0206
Update CHANGELOG.md
Lumabots May 2, 2025
b2b8884
add a get or fetch to guild
Lumabots May 3, 2025
8f9a257
Implement better get_or_fetch using typevar and object
Lumabots May 3, 2025
9f6219c
shortcut for get or fetch
Lumabots May 3, 2025
4af5149
Update CHANGELOG.md
Lumabots May 3, 2025
6f59fcd
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 3, 2025
c0689cc
usage of subclass
Lumabots May 3, 2025
b20822a
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 3, 2025
4a363f5
add get_emoji method
Lumabots May 3, 2025
c3a7dd2
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 3, 2025
e614c47
add get_or_fetch_emoji method
Lumabots May 3, 2025
0c0a042
shortcut getorfetch client + removal of get_or_fetch user in favor of…
Lumabots May 4, 2025
466e41b
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 4, 2025
388a234
Update utils.py
Lumabots May 4, 2025
9737f69
Update guild.py
Lumabots May 4, 2025
656de14
Update client.py
Lumabots May 4, 2025
461eab7
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 4, 2025
b6f8878
Update utils.py
Lumabots May 4, 2025
f2a7d72
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 4, 2025
5d68207
add utils.deprecated
Lumabots May 4, 2025
4ca87d8
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 4, 2025
9903fb7
add warning
Lumabots May 4, 2025
a1ec6f9
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 4, 2025
47de080
Update client.py
Lumabots May 4, 2025
4430825
added backward support
Lumabots May 5, 2025
010cc78
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 5, 2025
368a55d
fixed missing coma
Lumabots May 5, 2025
76c56a8
ig im drunk
Lumabots May 5, 2025
811682d
fix my drunkness
Lumabots May 5, 2025
a102d5d
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 5, 2025
528b84b
now im not drunk
Lumabots May 5, 2025
7e766be
Update utils.py
Lumabots May 5, 2025
f6b8e18
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 5, 2025
a5deffe
Update utils.py
Lumabots May 5, 2025
fff74c4
Update utils.py
Lumabots May 5, 2025
9e45627
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 5, 2025
0baae1d
usage of abc
Lumabots May 5, 2025
a3aeb4b
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 5, 2025
2404d9c
_EmojiTag
Lumabots May 5, 2025
23409a7
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 5, 2025
8f278f8
Update utils.py
Lumabots May 5, 2025
6d5be54
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 5, 2025
11fbbc9
Update utils.py
Lumabots May 9, 2025
590339a
Merge branch 'master' into get_or_fetch
Lumabots May 9, 2025
267d574
Update discord/client.py
Lumabots May 9, 2025
dc45449
add the raise error
Lumabots May 9, 2025
f22d8a2
Update discord/utils.py
Lumabots May 9, 2025
cbb7951
Update utils.pyfix missing appemoji
Lumabots May 9, 2025
fdbb7d3
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 9, 2025
5b35d95
add missing raise to doc
Lumabots May 9, 2025
b8a8628
refactor: add TODO comments for removal of deprecated arguments in ge…
Lumabots May 9, 2025
ba5541e
refactor: move _FETCHABLE type variable to utils for better accessibi…
Lumabots May 9, 2025
5421a3f
fix: update import statement for _FETCHABLE from utils module
Lumabots May 9, 2025
50318c2
refactor: streamline get_or_fetch logic with a mapping for object types
Lumabots May 9, 2025
541e668
fix: add validation for Guild type in get_or_fetch function
Lumabots May 9, 2025
b20cffb
fix: correct base type resolution in get_or_fetch function
Lumabots May 9, 2025
3e94a4f
fix: add Role type support in get_or_fetch function
Lumabots May 14, 2025
7384d7c
fix: update role retrieval methods in get_or_fetch function
Lumabots May 14, 2025
b832b55
Merge branch 'master' into get_or_fetch
Lumabots May 14, 2025
0ffaa4b
Update CHANGELOG.md
Lumabots May 15, 2025
92d2987
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 15, 2025
b8c7f7f
Update CHANGELOG.md
Lumabots May 17, 2025
53dc93c
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 17, 2025
6d89376
Update CHANGELOG.md
Lumabots May 17, 2025
e2e8b3d
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 17, 2025
2abfba2
Update CHANGELOG.md
Lumabots May 18, 2025
fb9e077
Merge branch 'master' into get_or_fetch
Lumabots May 20, 2025
c60fbab
feat: update get_or_fetch method to accept Optional[int] for object_id
Lumabots Jun 6, 2025
29863f4
Merge branch 'master' into get_or_fetch
Lumabots Jun 6, 2025
4a5eca4
fix: "MISSING" not being exported
Lumabots Jun 14, 2025
2ed0de6
Merge branch 'master' into get_or_fetch
Lumabots Jun 20, 2025
0c99851
Merge branch 'master' into get_or_fetch
Lumabots Jun 28, 2025
dffef1f
Merge branch 'master' into get_or_fetch
Lumabots Jul 13, 2025
f81a338
add some more comment
Lumabots Jul 13, 2025
cc273c2
Update utils.py
Lumabots Jul 13, 2025
0302fcd
Merge branch 'master' into get_or_fetch
Lumabots Jul 22, 2025
2837e94
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 22, 2025
1149c0a
Merge branch 'master' into get_or_fetch
Lumabots Aug 2, 2025
16fda2a
Merge branch 'master' into get_or_fetch
Lulalaby Aug 2, 2025
b70c666
Update discord/utils.py
Lumabots Aug 2, 2025
198556d
Update discord/utils.py
Lumabots Aug 2, 2025
91b3d68
comment
Lumabots Aug 2, 2025
cb38bd7
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 2, 2025
f13e86c
fix docs
Lumabots Aug 2, 2025
543dec8
Merge branch 'get_or_fetch' of https://github.com/Lumabots/pycord int…
Lumabots Aug 2, 2025
da59485
Update discord/client.py
Lumabots Aug 2, 2025
fa0efba
usage of literal None
Lumabots Aug 2, 2025
b6ff5f3
Merge branch 'get_or_fetch' of https://github.com/Lumabots/pycord int…
Lumabots Aug 2, 2025
7d12503
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ These changes are available on the `master` branch, but have not yet been releas
([#2714](https://github.com/Pycord-Development/pycord/pull/2714))
- Added the ability to pass a `datetime.time` object to `format_dt`
([#2747](https://github.com/Pycord-Development/pycord/pull/2747))
- Added `Guild.get_or_fetch()` shortcut method and better get_or_fetch
([#2776](https://github.com/Pycord-Development/pycord/pull/2776))

### Fixed

Expand Down
47 changes: 45 additions & 2 deletions discord/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,18 @@

if TYPE_CHECKING:
from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime
from .channel import DMChannel
from .channel import (
CategoryChannel,
DMChannel,
ForumChannel,
StageChannel,
TextChannel,
VoiceChannel,
)
from .member import Member
from .message import Message
from .poll import Poll
from .threads import Thread, ThreadMember
from .voice_client import VoiceProtocol

__all__ = ("Client",)
Expand Down Expand Up @@ -1113,6 +1121,10 @@ def get_all_members(self) -> Generator[Member]:
for guild in self.guilds:
yield from guild.members

@utils.deprecated(
instead="Client.get_or_fetch(User, id)",
since="2.7",
)
async def get_or_fetch_user(self, id: int, /) -> User | None:
"""|coro|
Expand All @@ -1129,7 +1141,38 @@ async def get_or_fetch_user(self, id: int, /) -> User | None:
The user or ``None`` if not found.
"""

return await utils.get_or_fetch(obj=self, attr="user", id=id, default=None)
return await utils.get_or_fetch(obj=self, attr=User, id=id, default=None)

_FETCHABLE = TypeVar(
"_FETCHABLE",
bound="VoiceChannel | TextChannel | ForumChannel | StageChannel | CategoryChannel | Thread | User | Guild | GuildEmoji | AppEmoji",
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import this from utils instead


async def get_or_fetch(
self: Client,
object_type: type[_FETCHABLE],
object_id: int,
default: Any = MISSING,
) -> _FETCHABLE | None:
"""Shortcut method to get data from guild object either by returning the cached version, or if it does not exist, attempt to fetch it from the api.
Parameters
----------
object_type: Union[:class:`VoiceChannel`, :class:`TextChannel`, :class:`ForumChannel`, :class:`StageChannel`, :class:`CategoryChannel`, :class:`Thread`, :class:`User`, :class:`Guild`, :class:`GuildEmoji`, :class:`AppEmoji`]
Type of object to fetch or get.
object_id: :class:`int`
ID of object to get.
default : Any, optional
A default to return instead of raising if fetch fails.
Returns
-------
Optional[Union[:class:`VoiceChannel`, :class:`TextChannel`, :class:`ForumChannel`, :class:`StageChannel`, :class:`CategoryChannel`, :class:`Thread`, :class:`User`, :class:`Guild`, :class:`GuildEmoji`, :class:`AppEmoji`]]
The object of type that was specified or ``None`` if not found.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use @utils.copy_doc

return await utils.get_or_fetch(
obj=self, object_type=object_type, object_id=object_id, default=default
)

# listeners/waiters

Expand Down
55 changes: 55 additions & 0 deletions discord/guild.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
Optional,
Sequence,
Tuple,
TypeVar,
Union,
overload,
)
Expand Down Expand Up @@ -863,6 +864,40 @@ def get_member(self, user_id: int, /) -> Member | None:
"""
return self._members.get(user_id)

_FETCHABLE = TypeVar(
"_FETCHABLE",
bound="VoiceChannel | TextChannel | ForumChannel | StageChannel | CategoryChannel | Thread | Member | GuildEmoji",
)

async def get_or_fetch(
self: Guild,
object_type: type[_FETCHABLE],
object_id: int,
default: Any = MISSING,
) -> _FETCHABLE | None:
"""Shortcut method to get data from guild object either by returning the cached version, or if it does not exist, attempt to fetch it from the api.

Parameters
----------
object_type: Union[:class:`VoiceChannel`, :class:`TextChannel`, :class:`ForumChannel`, :class:`StageChannel`, :class:`CategoryChannel`, :class:`Thread`, :class:`Member`, :class:`GuildEmoji`]
Type of object to fetch or get.

object_id: :class:`int`
ID of object to get.

default : Any, optional
A default to return instead of raising if fetch fails.

Returns
-------

Optional[Union[:class:`VoiceChannel`, :class:`TextChannel`, :class:`ForumChannel`, :class:`StageChannel`, :class:`CategoryChannel`, :class:`Thread`, :class:`Member`, :class:`GuildEmoji`]]
The object of type that was specified or ``None`` if not found.
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use @utils.copy_doc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should i use copy doc even tho its not the same exact doc ? the guild cannot use user and bot cannot use member

return await utils.get_or_fetch(
obj=self, object_type=object_type, object_id=object_id, default=default
)

@property
def premium_subscribers(self) -> list[Member]:
"""A list of members who have "boosted" this guild."""
Expand Down Expand Up @@ -2664,6 +2699,26 @@ async def delete_sticker(
"""
await self._state.http.delete_guild_sticker(self.id, sticker.id, reason)

def get_emoji(self, emoji_id: int, /) -> GuildEmoji | None:
"""Returns an emoji with the given ID.

.. versionadded:: 2.7

Parameters
----------
emoji_id: int
The ID to search for.

Returns
-------
Optional[:class:`Emoji`]
The returned Emoji or ``None`` if not found.
"""
emoji = self._state.get_emoji(emoji_id)
if emoji and emoji.guild == self:
return emoji
return None

async def fetch_emojis(self) -> list[GuildEmoji]:
r"""|coro|

Expand Down
166 changes: 123 additions & 43 deletions discord/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@
overload,
)

if TYPE_CHECKING:
from discord import (
Client,
VoiceChannel,
TextChannel,
ForumChannel,
StageChannel,
CategoryChannel,
Thread,
Member,
User,
Guild,
GuildEmoji,
)

from .errors import HTTPException, InvalidArgument

try:
Expand Down Expand Up @@ -573,64 +588,129 @@ def get(iterable: Iterable[T], **attrs: Any) -> T | None:
return None


async def get_or_fetch(obj, attr: str, id: int, *, default: Any = MISSING) -> Any:
"""|coro|
_FETCHABLE = TypeVar(
"_FETCHABLE",
bound="VoiceChannel | TextChannel | ForumChannel | StageChannel | CategoryChannel | Thread | Member | User | Guild | GuildEmoji | AppEmoji",
)


Attempts to get an attribute from the object in cache. If it fails, it will attempt to fetch it.
If the fetch also fails, an error will be raised.
# TODO REMOVE THE FOR object_type and object_id + remove both attr and id after depreciation
async def get_or_fetch(
obj: Guild | Client,
object_type: type[_FETCHABLE] = MISSING,
object_id: int = MISSING,
default: Any = MISSING,
attr: str = MISSING,
id: int = MISSING,
) -> _FETCHABLE | None:
"""
Shortcut method to get data from guild object either by returning the cached version, or if it does not exist, attempt to fetch it from the api.

Parameters
----------
obj: Any
The object to use the get or fetch methods in
attr: :class:`str`
The attribute to get or fetch. Note the object must have both a ``get_`` and ``fetch_`` method for this attribute.
id: :class:`int`
The ID of the object
default: Any
The default value to return if the object is not found, instead of raising an error.
obj : Guild | Client
The object to operate on.
object_type: Union[:class:`VoiceChannel`, :class:`TextChannel`, :class:`ForumChannel`, :class:`StageChannel`, :class:`CategoryChannel`, :class:`Thread`, :class:`User`, :class:`Guild`, :class:`Member`, :class:`GuildEmoji`, :class:`AppEmoji`]
Type of object to fetch or get.

object_id: :class:`int`
ID of object to get.

default : Any, optional
A default to return instead of raising if fetch fails.

Returns
-------
Any
The object found or the default value.

Raises
------
:exc:`AttributeError`
The object is missing a ``get_`` or ``fetch_`` method
:exc:`NotFound`
Invalid ID for the object
:exc:`HTTPException`
An error occurred fetching the object
:exc:`Forbidden`
You do not have permission to fetch the object

Examples
--------

Getting a guild from a guild ID: ::
Optional[Union[:class:`VoiceChannel`, :class:`TextChannel`, :class:`ForumChannel`, :class:`StageChannel`, :class:`CategoryChannel`, :class:`Thread`, :class:`User`, :class:`Guild`, :class:`Member`, :class:`GuildEmoji`, :class:`AppEmoji`]]
The object of type that was specified or ``None`` if not found.
"""
from discord import (
AppEmoji,
CategoryChannel,
ForumChannel,
Guild,
GuildEmoji,
Member,
StageChannel,
TextChannel,
Thread,
User,
VoiceChannel,
)

guild = await utils.get_or_fetch(client, 'guild', guild_id)
# TODO REMOVE THIS PART AfTER DEPREcIATION
string_to_type = {
"channel": TextChannel,
"member": Member,
"user": User,
"guild": Guild,
"emoji": GuildEmoji,
"appemoji": AppEmoji,
}

if attr is not MISSING or id is not MISSING or isinstance(object_type, str):
warn_deprecated(
name="get_or_fetch(obj, attr='type', id=...)",
instead="get_or_fetch(obj, object_type=Type, object_id=...)",
since="2.7",
)
attr = object_type if object_type is not MISSING else attr
mapped_type = string_to_type.get(attr.lower())
if mapped_type is None:
raise InvalidArgument(
f"Unknown type string '{attr}' passed as `attr`. Use a valid object class instead."
)
object_type = mapped_type
object_id = id

if object_type is MISSING or object_id is MISSING:
raise TypeError("required parameters: `object_type` and `object_id`.")
# Util here

if issubclass(object_type, (Member, User, Guild)):
attr = object_type.__name__.lower()
elif issubclass(object_type, (GuildEmoji, AppEmoji)):
attr = "emoji"
elif issubclass(
object_type,
(
VoiceChannel,
TextChannel,
ForumChannel,
StageChannel,
CategoryChannel,
Thread,
),
):
attr = "channel"
else:
raise InvalidArgument(
f"Class {object_type.__name__} cannot be used with discord.{type(obj).__name__}.get_or_fetch()"
)
if isinstance(obj, Guild) and object_type is User:
raise InvalidArgument(
"Guild cannot get_or_fetch discord.User. Use Client instead."
)
elif isinstance(obj, Client) and object_type is Member:
raise InvalidArgument("Client cannot get_or_fetch Member. Use Guild instead.")

Getting a channel from the guild. If the channel is not found, return None: ::
getter = getattr(obj, f"get_{attr}", None)
if getter:
result = getter(object_id)
if result is not None:
return result

channel = await utils.get_or_fetch(guild, 'channel', channel_id, default=None)
"""
getter = getattr(obj, f"get_{attr}")(id)
if getter is None:
fetcher = getattr(obj, f"fetch_{attr}", None) or getattr(
obj, f"_fetch_{attr}", None
)
if fetcher:
try:
getter = await getattr(obj, f"fetch_{attr}")(id)
except AttributeError:
getter = await getattr(obj, f"_fetch_{attr}")(id)
if getter is None:
raise ValueError(f"Could not find {attr} with id {id} on {obj}")
return await fetcher(object_id)
except (HTTPException, ValueError):
if default is not MISSING:
return default
else:
raise
return getter
raise


def _unique(iterable: Iterable[T]) -> list[T]:
Expand Down