raycord 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raycord/__init__.py +310 -0
- raycord/_callbacks.py +141 -0
- raycord/_state.py +74 -0
- raycord/_task.py +112 -0
- raycord/bot.py +297 -0
- raycord/checks.py +251 -0
- raycord/cogs.py +184 -0
- raycord/commands_api.py +501 -0
- raycord/errors.py +23 -0
- raycord/events.py +128 -0
- raycord/guild.py +268 -0
- raycord/intents.py +56 -0
- raycord/messages.py +142 -0
- raycord/py.typed +0 -0
- raycord/ui.py +379 -0
- raycord/voice.py +186 -0
- raycord-0.1.0.dist-info/METADATA +268 -0
- raycord-0.1.0.dist-info/RECORD +20 -0
- raycord-0.1.0.dist-info/WHEEL +4 -0
- raycord-0.1.0.dist-info/licenses/LICENCE.txt +21 -0
raycord/__init__.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""raycord - a raylib-simple wrapper around discord.py.
|
|
2
|
+
|
|
3
|
+
Quick start::
|
|
4
|
+
|
|
5
|
+
import raycord
|
|
6
|
+
|
|
7
|
+
raycord.init("YOUR_TOKEN", prefix="!")
|
|
8
|
+
|
|
9
|
+
@raycord.on_ready
|
|
10
|
+
def ready():
|
|
11
|
+
print("online as", raycord.user())
|
|
12
|
+
|
|
13
|
+
@raycord.command("ping")
|
|
14
|
+
def ping(ctx):
|
|
15
|
+
raycord.reply(ctx, "pong")
|
|
16
|
+
|
|
17
|
+
raycord.run()
|
|
18
|
+
|
|
19
|
+
Everything is a plain function; nothing needs ``await`` unless you want to
|
|
20
|
+
wait for a result (every helper returns a :class:`RayTask`).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import discord
|
|
28
|
+
from discord import app_commands
|
|
29
|
+
from discord.ext import commands
|
|
30
|
+
|
|
31
|
+
from ._callbacks import (
|
|
32
|
+
ensure_async,
|
|
33
|
+
is_context,
|
|
34
|
+
is_interaction,
|
|
35
|
+
resolve_channel,
|
|
36
|
+
resolve_guild,
|
|
37
|
+
resolve_member,
|
|
38
|
+
resolve_voice,
|
|
39
|
+
)
|
|
40
|
+
from ._task import RayTask
|
|
41
|
+
from .bot import (
|
|
42
|
+
RaycordAutoShardedBot,
|
|
43
|
+
RaycordBot,
|
|
44
|
+
add_listener,
|
|
45
|
+
application,
|
|
46
|
+
bot,
|
|
47
|
+
get_channel,
|
|
48
|
+
get_guild,
|
|
49
|
+
get_member,
|
|
50
|
+
get_prefix,
|
|
51
|
+
get_user,
|
|
52
|
+
guilds,
|
|
53
|
+
init,
|
|
54
|
+
is_ready,
|
|
55
|
+
make_bot,
|
|
56
|
+
reset,
|
|
57
|
+
run,
|
|
58
|
+
set_status,
|
|
59
|
+
shutdown,
|
|
60
|
+
sync,
|
|
61
|
+
user,
|
|
62
|
+
wait_until_ready,
|
|
63
|
+
)
|
|
64
|
+
from .checks import (
|
|
65
|
+
bot_has_permissions,
|
|
66
|
+
check,
|
|
67
|
+
cooldown,
|
|
68
|
+
dm_only,
|
|
69
|
+
guild_only,
|
|
70
|
+
has_any_role,
|
|
71
|
+
has_guild_permissions,
|
|
72
|
+
has_permissions,
|
|
73
|
+
has_role,
|
|
74
|
+
is_owner,
|
|
75
|
+
nsfw_only,
|
|
76
|
+
)
|
|
77
|
+
from .cogs import Cog, add_cog, cog, cogs, reload_cog, remove_cog
|
|
78
|
+
from .commands_api import (
|
|
79
|
+
SlashGroup,
|
|
80
|
+
autocomplete,
|
|
81
|
+
choices,
|
|
82
|
+
command,
|
|
83
|
+
describe,
|
|
84
|
+
group,
|
|
85
|
+
message_command,
|
|
86
|
+
rename,
|
|
87
|
+
slash,
|
|
88
|
+
slash_group,
|
|
89
|
+
user_command,
|
|
90
|
+
)
|
|
91
|
+
from .errors import (
|
|
92
|
+
AlreadyInitialized,
|
|
93
|
+
NotInitialized,
|
|
94
|
+
NotReady,
|
|
95
|
+
RaycordError,
|
|
96
|
+
TargetError,
|
|
97
|
+
)
|
|
98
|
+
from .events import *
|
|
99
|
+
from .events import on, on_app_error
|
|
100
|
+
from .guild import (
|
|
101
|
+
ban,
|
|
102
|
+
create_category,
|
|
103
|
+
create_channel,
|
|
104
|
+
create_emoji,
|
|
105
|
+
create_event,
|
|
106
|
+
create_forum,
|
|
107
|
+
create_forum_post,
|
|
108
|
+
create_role,
|
|
109
|
+
create_sticker,
|
|
110
|
+
create_text_channel,
|
|
111
|
+
create_thread,
|
|
112
|
+
create_voice_channel,
|
|
113
|
+
kick,
|
|
114
|
+
unban,
|
|
115
|
+
)
|
|
116
|
+
from .intents import PRESETS, intents
|
|
117
|
+
from .messages import (
|
|
118
|
+
dm,
|
|
119
|
+
fetch_message,
|
|
120
|
+
pin,
|
|
121
|
+
react,
|
|
122
|
+
reply,
|
|
123
|
+
send,
|
|
124
|
+
)
|
|
125
|
+
from .ui import (
|
|
126
|
+
RayModal,
|
|
127
|
+
button,
|
|
128
|
+
field,
|
|
129
|
+
modal,
|
|
130
|
+
on_button,
|
|
131
|
+
on_modal,
|
|
132
|
+
on_select,
|
|
133
|
+
select,
|
|
134
|
+
show_modal,
|
|
135
|
+
view,
|
|
136
|
+
)
|
|
137
|
+
from .voice import (
|
|
138
|
+
is_playing,
|
|
139
|
+
join_voice,
|
|
140
|
+
leave_voice,
|
|
141
|
+
pause_voice,
|
|
142
|
+
play,
|
|
143
|
+
resume_voice,
|
|
144
|
+
stop_voice,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
__version__ = "0.1.0"
|
|
148
|
+
|
|
149
|
+
Intents = discord.Intents
|
|
150
|
+
|
|
151
|
+
# ``edit`` and ``delete`` dispatch to the message helpers for messages and
|
|
152
|
+
# interactions, and to the generic guild helpers for everything else.
|
|
153
|
+
from . import guild as _guild # noqa: E402
|
|
154
|
+
from . import messages as _messages # noqa: E402
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def edit(target: Any, **fields: Any) -> RayTask:
|
|
158
|
+
"""Edit a message or interaction reply, or any other editable object."""
|
|
159
|
+
if isinstance(target, (discord.Message, discord.Interaction, commands.Context)):
|
|
160
|
+
return _messages.edit(target, **fields)
|
|
161
|
+
return _guild.edit(target, **fields)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def delete(target: Any, *, reason: Any = None) -> RayTask:
|
|
165
|
+
"""Delete a message or interaction reply, or any other deletable object."""
|
|
166
|
+
if isinstance(target, (discord.Message, discord.Interaction, commands.Context)):
|
|
167
|
+
return _messages.delete(target)
|
|
168
|
+
return _guild.delete(target, reason=reason)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Friendly aliases: raycord reads like a small game loop.
|
|
172
|
+
start = run
|
|
173
|
+
launch = run
|
|
174
|
+
stop = shutdown
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = [
|
|
178
|
+
# modules and core types
|
|
179
|
+
"discord",
|
|
180
|
+
"commands",
|
|
181
|
+
"app_commands",
|
|
182
|
+
"RayTask",
|
|
183
|
+
"RaycordError",
|
|
184
|
+
"AlreadyInitialized",
|
|
185
|
+
"NotInitialized",
|
|
186
|
+
"NotReady",
|
|
187
|
+
"TargetError",
|
|
188
|
+
"__version__",
|
|
189
|
+
# lifecycle
|
|
190
|
+
"init",
|
|
191
|
+
"run",
|
|
192
|
+
"start",
|
|
193
|
+
"launch",
|
|
194
|
+
"shutdown",
|
|
195
|
+
"stop",
|
|
196
|
+
"reset",
|
|
197
|
+
"bot",
|
|
198
|
+
"make_bot",
|
|
199
|
+
"RaycordBot",
|
|
200
|
+
"RaycordAutoShardedBot",
|
|
201
|
+
"is_ready",
|
|
202
|
+
"wait_until_ready",
|
|
203
|
+
"sync",
|
|
204
|
+
"add_listener",
|
|
205
|
+
"set_status",
|
|
206
|
+
"user",
|
|
207
|
+
"application",
|
|
208
|
+
"guilds",
|
|
209
|
+
"get_guild",
|
|
210
|
+
"get_channel",
|
|
211
|
+
"get_user",
|
|
212
|
+
"get_member",
|
|
213
|
+
"get_prefix",
|
|
214
|
+
# intents
|
|
215
|
+
"intents",
|
|
216
|
+
"Intents",
|
|
217
|
+
"PRESETS",
|
|
218
|
+
# commands
|
|
219
|
+
"command",
|
|
220
|
+
"slash",
|
|
221
|
+
"slash_group",
|
|
222
|
+
"SlashGroup",
|
|
223
|
+
"group",
|
|
224
|
+
"user_command",
|
|
225
|
+
"message_command",
|
|
226
|
+
"describe",
|
|
227
|
+
"rename",
|
|
228
|
+
"autocomplete",
|
|
229
|
+
"choices",
|
|
230
|
+
# checks
|
|
231
|
+
"check",
|
|
232
|
+
"cooldown",
|
|
233
|
+
"has_permissions",
|
|
234
|
+
"has_guild_permissions",
|
|
235
|
+
"bot_has_permissions",
|
|
236
|
+
"has_role",
|
|
237
|
+
"has_any_role",
|
|
238
|
+
"is_owner",
|
|
239
|
+
"guild_only",
|
|
240
|
+
"dm_only",
|
|
241
|
+
"nsfw_only",
|
|
242
|
+
# events
|
|
243
|
+
"on",
|
|
244
|
+
"on_app_error",
|
|
245
|
+
# messages
|
|
246
|
+
"send",
|
|
247
|
+
"reply",
|
|
248
|
+
"edit",
|
|
249
|
+
"delete",
|
|
250
|
+
"react",
|
|
251
|
+
"pin",
|
|
252
|
+
"dm",
|
|
253
|
+
"fetch_message",
|
|
254
|
+
# ui
|
|
255
|
+
"view",
|
|
256
|
+
"button",
|
|
257
|
+
"select",
|
|
258
|
+
"field",
|
|
259
|
+
"modal",
|
|
260
|
+
"RayModal",
|
|
261
|
+
"show_modal",
|
|
262
|
+
"on_button",
|
|
263
|
+
"on_select",
|
|
264
|
+
"on_modal",
|
|
265
|
+
# voice
|
|
266
|
+
"join_voice",
|
|
267
|
+
"leave_voice",
|
|
268
|
+
"play",
|
|
269
|
+
"stop_voice",
|
|
270
|
+
"pause_voice",
|
|
271
|
+
"resume_voice",
|
|
272
|
+
"is_playing",
|
|
273
|
+
# guild
|
|
274
|
+
"create_channel",
|
|
275
|
+
"create_text_channel",
|
|
276
|
+
"create_voice_channel",
|
|
277
|
+
"create_category",
|
|
278
|
+
"create_forum",
|
|
279
|
+
"create_thread",
|
|
280
|
+
"create_forum_post",
|
|
281
|
+
"create_role",
|
|
282
|
+
"create_emoji",
|
|
283
|
+
"create_sticker",
|
|
284
|
+
"create_event",
|
|
285
|
+
"kick",
|
|
286
|
+
"ban",
|
|
287
|
+
"unban",
|
|
288
|
+
# cogs
|
|
289
|
+
"Cog",
|
|
290
|
+
"add_cog",
|
|
291
|
+
"remove_cog",
|
|
292
|
+
"reload_cog",
|
|
293
|
+
"cog",
|
|
294
|
+
"cogs",
|
|
295
|
+
# helpers
|
|
296
|
+
"ensure_async",
|
|
297
|
+
"resolve_member",
|
|
298
|
+
"resolve_guild",
|
|
299
|
+
"resolve_channel",
|
|
300
|
+
"resolve_voice",
|
|
301
|
+
"is_interaction",
|
|
302
|
+
"is_context",
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
# Pull the generated event decorators (on_ready, on_message, ...) into __all__.
|
|
306
|
+
__all__ += [
|
|
307
|
+
name
|
|
308
|
+
for name in dir()
|
|
309
|
+
if name.startswith("on_") and name not in __all__ and not name.startswith("__")
|
|
310
|
+
]
|
raycord/_callbacks.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Helpers that normalize user callbacks and resolve Discord targets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import inspect
|
|
7
|
+
import typing
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import discord
|
|
11
|
+
from discord.ext import commands as dpy_commands
|
|
12
|
+
|
|
13
|
+
from .errors import TargetError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ensure_async(fn: Any) -> Any:
|
|
17
|
+
"""Return an async callable that behaves like ``fn``.
|
|
18
|
+
|
|
19
|
+
Works for plain functions, bound methods and already-async callables. The
|
|
20
|
+
wrapper keeps ``fn``'s signature (via ``functools.wraps``) so discord.py's
|
|
21
|
+
converters and slash-command parameter extraction still work, and it hides
|
|
22
|
+
class membership so discord.py treats bound methods as plain callables.
|
|
23
|
+
"""
|
|
24
|
+
@functools.wraps(fn)
|
|
25
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
26
|
+
result = fn(*args, **kwargs)
|
|
27
|
+
if inspect.isawaitable(result):
|
|
28
|
+
return await result
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
# Resolve string annotations now, using the original module's globals.
|
|
32
|
+
try:
|
|
33
|
+
hints = typing.get_type_hints(fn, include_extras=True)
|
|
34
|
+
except Exception: # pragma: no cover - exotic annotations
|
|
35
|
+
hints = {}
|
|
36
|
+
merged = dict(getattr(fn, "__annotations__", {}) or {})
|
|
37
|
+
merged.update(hints)
|
|
38
|
+
if merged:
|
|
39
|
+
wrapper.__annotations__ = merged
|
|
40
|
+
|
|
41
|
+
# Prevent discord.py from treating the wrapper as a class method.
|
|
42
|
+
wrapper.__qualname__ = wrapper.__name__
|
|
43
|
+
|
|
44
|
+
# Copy flags such as app_commands.describe metadata set on the original.
|
|
45
|
+
source = getattr(fn, "__dict__", None)
|
|
46
|
+
if source is None and hasattr(fn, "__func__"):
|
|
47
|
+
source = getattr(fn.__func__, "__dict__", None)
|
|
48
|
+
if source:
|
|
49
|
+
wrapper.__dict__.update(source)
|
|
50
|
+
|
|
51
|
+
return wrapper
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_interaction(obj: Any) -> bool:
|
|
55
|
+
return isinstance(obj, discord.Interaction)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_context(obj: Any) -> bool:
|
|
59
|
+
return isinstance(obj, dpy_commands.Context)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def is_class_method(fn: Any) -> bool:
|
|
63
|
+
"""Return ``True`` for functions defined directly in a class body.
|
|
64
|
+
|
|
65
|
+
Nested functions (``outer.<locals>.fn``) are not class methods, so they can
|
|
66
|
+
be registered immediately just like module-level functions.
|
|
67
|
+
"""
|
|
68
|
+
qualname = getattr(fn, "__qualname__", getattr(fn, "__name__", ""))
|
|
69
|
+
parts = qualname.split(".")
|
|
70
|
+
if len(parts) < 2:
|
|
71
|
+
return False
|
|
72
|
+
return parts[-2] != "<locals>"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_member(obj: Any) -> Optional[discord.abc.User]:
|
|
76
|
+
"""Best-effort "who triggered this": author, user or the user itself."""
|
|
77
|
+
if obj is None:
|
|
78
|
+
return None
|
|
79
|
+
if isinstance(obj, (discord.Member, discord.User)):
|
|
80
|
+
return obj
|
|
81
|
+
author = getattr(obj, "author", None)
|
|
82
|
+
if author is not None:
|
|
83
|
+
return author
|
|
84
|
+
user = getattr(obj, "user", None)
|
|
85
|
+
if user is not None:
|
|
86
|
+
return user
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def resolve_guild(obj: Any) -> Optional[discord.Guild]:
|
|
91
|
+
"""Best-effort guild lookup for contexts, interactions and messages."""
|
|
92
|
+
if obj is None:
|
|
93
|
+
return None
|
|
94
|
+
if isinstance(obj, discord.Guild):
|
|
95
|
+
return obj
|
|
96
|
+
guild = getattr(obj, "guild", None)
|
|
97
|
+
if guild is not None:
|
|
98
|
+
return guild
|
|
99
|
+
channel = getattr(obj, "channel", None)
|
|
100
|
+
if channel is not None:
|
|
101
|
+
return getattr(channel, "guild", None)
|
|
102
|
+
member = resolve_member(obj)
|
|
103
|
+
if member is not None:
|
|
104
|
+
return getattr(member, "guild", None)
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def resolve_channel(obj: Any) -> Optional[Any]:
|
|
109
|
+
"""Best-effort channel lookup for contexts, interactions and messages."""
|
|
110
|
+
if obj is None:
|
|
111
|
+
return None
|
|
112
|
+
if isinstance(obj, discord.abc.GuildChannel):
|
|
113
|
+
return obj
|
|
114
|
+
channel = getattr(obj, "channel", None)
|
|
115
|
+
if channel is not None:
|
|
116
|
+
return channel
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resolve_voice(obj: Any) -> Optional[Any]:
|
|
121
|
+
"""Return the voice *channel* the target's author is sitting in."""
|
|
122
|
+
member = resolve_member(obj)
|
|
123
|
+
if member is None:
|
|
124
|
+
return None
|
|
125
|
+
voice = getattr(member, "voice", None)
|
|
126
|
+
if voice is None:
|
|
127
|
+
return None
|
|
128
|
+
return getattr(voice, "channel", None)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def require_bot(obj: Any = None) -> Any:
|
|
132
|
+
"""Return a bot instance from ``obj`` or from the global state."""
|
|
133
|
+
from . import _state
|
|
134
|
+
|
|
135
|
+
if obj is not None:
|
|
136
|
+
bot = getattr(obj, "bot", None) or getattr(obj, "client", None)
|
|
137
|
+
if bot is not None:
|
|
138
|
+
return bot
|
|
139
|
+
if _state.state.bot is None:
|
|
140
|
+
raise TargetError("no bot yet; call raycord.init(token) first")
|
|
141
|
+
return _state.state.bot
|
raycord/_state.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Global, raylib-style state for raycord.
|
|
2
|
+
|
|
3
|
+
raylib keeps one implicit context; raycord does the same. There is a single
|
|
4
|
+
:data:`state` object holding the bot, the event loop and every registration
|
|
5
|
+
that has to be replayed once the bot is ready.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class State:
|
|
15
|
+
"""The implicit raycord context."""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self.bot: Any = None
|
|
19
|
+
self.loop: Any = None
|
|
20
|
+
self.token: Optional[str] = None
|
|
21
|
+
self.prefix: Any = None
|
|
22
|
+
self.config: Dict[str, Any] = {}
|
|
23
|
+
self.registrations: List[Callable[[Any], Any]] = []
|
|
24
|
+
self.flushed = False
|
|
25
|
+
self.ready = False
|
|
26
|
+
self.closed = False
|
|
27
|
+
# Global UI routing tables: custom_id -> handler.
|
|
28
|
+
self.ui_handlers: Dict[str, Dict[str, Callable[..., Any]]] = {
|
|
29
|
+
"button": {},
|
|
30
|
+
"select": {},
|
|
31
|
+
"modal": {},
|
|
32
|
+
}
|
|
33
|
+
# name -> record describing everything a loaded cog created.
|
|
34
|
+
self.cogs: Dict[str, Dict[str, Any]] = {}
|
|
35
|
+
|
|
36
|
+
def reset(self) -> None:
|
|
37
|
+
"""Forget everything. Mostly useful for tests and restarts."""
|
|
38
|
+
self.bot = None
|
|
39
|
+
self.loop = None
|
|
40
|
+
self.token = None
|
|
41
|
+
self.prefix = None
|
|
42
|
+
self.config = {}
|
|
43
|
+
self.registrations = []
|
|
44
|
+
self.flushed = False
|
|
45
|
+
self.ready = False
|
|
46
|
+
self.closed = False
|
|
47
|
+
self.ui_handlers = {"button": {}, "select": {}, "modal": {}}
|
|
48
|
+
self.cogs = {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
state = State()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def when_bot(fn: Callable[[Any], Any]) -> Any:
|
|
55
|
+
"""Run ``fn(bot)`` now if the bot exists, otherwise when it is ready.
|
|
56
|
+
|
|
57
|
+
``fn`` may return an awaitable; :func:`flush` will await it.
|
|
58
|
+
"""
|
|
59
|
+
if state.flushed and state.bot is not None:
|
|
60
|
+
return fn(state.bot)
|
|
61
|
+
state.registrations.append(fn)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def flush(bot: Any) -> None:
|
|
66
|
+
"""Replay every pending registration against ``bot``."""
|
|
67
|
+
state.bot = bot
|
|
68
|
+
pending, state.registrations = state.registrations, []
|
|
69
|
+
for fn in pending:
|
|
70
|
+
result = fn(bot)
|
|
71
|
+
if inspect.isawaitable(result):
|
|
72
|
+
await result
|
|
73
|
+
state.flushed = True
|
|
74
|
+
state.closed = False
|
raycord/_task.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""A tiny "started coroutine" object, so raycord functions never need ``await``.
|
|
2
|
+
|
|
3
|
+
raylib never asks you to wait; you just call the function. ``RayTask`` lets a
|
|
4
|
+
raycord helper start an async discord.py call right away and still be usable
|
|
5
|
+
from three situations:
|
|
6
|
+
|
|
7
|
+
* fire and forget ``raycord.send(channel, "hi")``
|
|
8
|
+
* blocking sync code ``raycord.send(channel, "hi").result()``
|
|
9
|
+
* async code ``await raycord.send(channel, "hi")``
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import concurrent.futures
|
|
16
|
+
import inspect
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from . import _state
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RayTask:
|
|
23
|
+
"""Handle for a discord.py coroutine that already started running."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, coro: Any, loop: Any = None) -> None:
|
|
26
|
+
self._coro = coro
|
|
27
|
+
self._async: Optional[asyncio.Future] = None
|
|
28
|
+
self._future: Optional[concurrent.futures.Future] = None
|
|
29
|
+
|
|
30
|
+
loop = loop or _state.state.loop
|
|
31
|
+
|
|
32
|
+
if not inspect.isawaitable(coro):
|
|
33
|
+
done: concurrent.futures.Future = concurrent.futures.Future()
|
|
34
|
+
done.set_result(coro)
|
|
35
|
+
self._future = done
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
running = asyncio.get_running_loop()
|
|
40
|
+
except RuntimeError:
|
|
41
|
+
running = None
|
|
42
|
+
|
|
43
|
+
if running is not None:
|
|
44
|
+
# Called from inside the bot loop; use it directly.
|
|
45
|
+
self._async = asyncio.ensure_future(coro)
|
|
46
|
+
elif loop is not None and loop.is_running():
|
|
47
|
+
# Called from another thread; hand the work to the bot loop.
|
|
48
|
+
self._future = asyncio.run_coroutine_threadsafe(coro, loop) # type: ignore[arg-type]
|
|
49
|
+
else:
|
|
50
|
+
# No bot around (yet). Best effort: run it to completion now.
|
|
51
|
+
self._future = concurrent.futures.Future()
|
|
52
|
+
try:
|
|
53
|
+
self._future.set_result(asyncio.run(coro)) # type: ignore[arg-type]
|
|
54
|
+
except BaseException as exc: # pragma: no cover - defensive
|
|
55
|
+
self._future.set_exception(exc)
|
|
56
|
+
|
|
57
|
+
# -- inspection ------------------------------------------------------
|
|
58
|
+
def done(self) -> bool:
|
|
59
|
+
"""Return ``True`` when the underlying work has finished."""
|
|
60
|
+
if self._async is not None:
|
|
61
|
+
return self._async.done()
|
|
62
|
+
assert self._future is not None
|
|
63
|
+
return self._future.done()
|
|
64
|
+
|
|
65
|
+
def cancelled(self) -> bool:
|
|
66
|
+
"""Return ``True`` if the work was cancelled."""
|
|
67
|
+
if self._async is not None:
|
|
68
|
+
return self._async.cancelled()
|
|
69
|
+
assert self._future is not None
|
|
70
|
+
return self._future.cancelled()
|
|
71
|
+
|
|
72
|
+
def cancel(self) -> bool:
|
|
73
|
+
"""Attempt to cancel the work."""
|
|
74
|
+
if self._async is not None:
|
|
75
|
+
return self._async.cancel()
|
|
76
|
+
assert self._future is not None
|
|
77
|
+
return self._future.cancel()
|
|
78
|
+
|
|
79
|
+
def result(self, timeout: Optional[float] = None) -> Any:
|
|
80
|
+
"""Block until the work finishes and return its value.
|
|
81
|
+
|
|
82
|
+
Only safe outside the event loop thread. From async code use
|
|
83
|
+
``await`` on the task instead.
|
|
84
|
+
"""
|
|
85
|
+
if self._async is not None:
|
|
86
|
+
if not self._async.done():
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
"result() cannot block the event loop; await the RayTask instead"
|
|
89
|
+
)
|
|
90
|
+
return self._async.result()
|
|
91
|
+
assert self._future is not None
|
|
92
|
+
return self._future.result(timeout)
|
|
93
|
+
|
|
94
|
+
def exception(self, timeout: Optional[float] = None) -> Optional[BaseException]:
|
|
95
|
+
"""Return the exception raised by the work, if any."""
|
|
96
|
+
if self._async is not None:
|
|
97
|
+
if not self._async.done():
|
|
98
|
+
raise RuntimeError("task is not done; await the RayTask instead")
|
|
99
|
+
return self._async.exception()
|
|
100
|
+
assert self._future is not None
|
|
101
|
+
return self._future.exception(timeout)
|
|
102
|
+
|
|
103
|
+
# -- awaiting --------------------------------------------------------
|
|
104
|
+
def __await__(self):
|
|
105
|
+
if self._async is not None:
|
|
106
|
+
return self._async.__await__()
|
|
107
|
+
assert self._future is not None
|
|
108
|
+
return asyncio.wrap_future(self._future).__await__()
|
|
109
|
+
|
|
110
|
+
def __repr__(self) -> str: # pragma: no cover - cosmetic
|
|
111
|
+
state = "pending" if not self.done() else "done"
|
|
112
|
+
return f"<RayTask {state}>"
|