Skip to content

Commit 0a39352

Browse files
authored
Merge branch 'master' into master
2 parents 100126f + c5bee66 commit 0a39352

File tree

19 files changed

+469
-46
lines changed

19 files changed

+469
-46
lines changed

.github/CONTRIBUTING.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ If the bug report is missing this information then it'll take us longer to fix t
3434

3535
Submitting a pull request is fairly simple, just make sure it focuses on a single aspect and doesn't manage to have scope creep and it's probably good to go. It would be incredibly lovely if the style is consistent to that found in the project. This project follows PEP-8 guidelines (mostly) with a column limit of 125.
3636

37+
## Use of "type: ignore" comments
38+
In some cases, it might be necessary to ignore type checker warnings for one reason or another.
39+
If that is that case, it is **required** that a comment is left explaining why you are
40+
deciding to ignore type checking warnings.
41+
3742
### Licensing
3843

3944
By submitting a pull request, you agree that; 1) You hold the copyright on all submitted code inside said pull request; 2) You agree to transfer all rights to the owner of this repository, and; 3) If you are found to be in fault with any of the above, we shall not be held responsible in any way after the pull request has been merged.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
- [ ] If code changes were made then they have been tested.
1010
- [ ] I have updated the documentation to reflect the changes.
11+
- [ ] If `type: ignore` comments were used, a comment is also left explaining why
1112
- [ ] This PR fixes an issue.
1213
- [ ] This PR adds something new (e.g. new method or parameters).
1314
- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed)

.github/workflows/bandit.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: bandit
2+
on: [pull_request, push]
3+
jobs:
4+
bandit:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v2
8+
- uses: actions/setup-python@v2
9+
- run: pip install bandit
10+
- run: bandit --recursive --skip B101,B104,B105,B110,B307,B311,B404,B603,B607 .

.github/workflows/mypy.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: mypy
2+
on: [pull_request, push]
3+
jobs:
4+
mypy:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v2
8+
- uses: actions/setup-python@v2
9+
- run: pip install mypy
10+
- run: pip install -r requirements.txt
11+
- run: mkdir --parents --verbose .mypy_cache
12+
- run: mypy --ignore-missing-imports --install-types --non-interactive . || true

.pylintrc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[MESSAGES CONTROL]
2+
3+
disable=protected-access
4+
5+
enable=bad-indentation,line-too-long
6+
7+
8+
[FORMAT]
9+
10+
indent-string=' '
11+
12+
max-line-length=120

discord/bot.py

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
Coroutine,
3838
List,
3939
Optional,
40+
Type,
4041
TypeVar,
4142
Union,
4243
)
@@ -53,6 +54,7 @@
5354
UserCommand,
5455
ApplicationCommand,
5556
ApplicationContext,
57+
AutocompleteContext,
5658
command,
5759
)
5860
from .cog import CogMixin
@@ -86,19 +88,23 @@ class ApplicationCommandMixin:
8688
def __init__(self, *args, **kwargs) -> None:
8789
super().__init__(*args, **kwargs)
8890
self._pending_application_commands = []
89-
self.application_commands = {}
91+
self._application_commands = {}
9092

9193
@property
9294
def pending_application_commands(self):
9395
return self._pending_application_commands
9496

9597
@property
9698
def commands(self) -> List[Union[ApplicationCommand, Any]]:
97-
commands = list(self.application_commands.values())
99+
commands = self.application_commands
98100
if self._supports_prefixed_commands:
99101
commands += self.prefixed_commands
100102
return commands
101103

104+
@property
105+
def application_commands(self) -> List[ApplicationCommand]:
106+
return list(self._application_commands.values())
107+
102108
def add_application_command(self, command: ApplicationCommand) -> None:
103109
"""Adds a :class:`.ApplicationCommand` into the internal list of commands.
104110
@@ -112,7 +118,6 @@ def add_application_command(self, command: ApplicationCommand) -> None:
112118
command: :class:`.ApplicationCommand`
113119
The command to add.
114120
"""
115-
116121
if self.debug_guilds and command.guild_ids is None:
117122
command.guild_ids = self.debug_guilds
118123
self._pending_application_commands.append(command)
@@ -136,7 +141,54 @@ def remove_application_command(
136141
The command that was removed. If the name is not valid then
137142
``None`` is returned instead.
138143
"""
139-
return self.application_commands.pop(command.id)
144+
return self._application_commands.pop(command.id)
145+
146+
@property
147+
def get_command(self):
148+
"""Shortcut for :meth:`.get_application_command`.
149+
150+
.. note::
151+
Overridden in :class:`ext.commands.Bot`.
152+
153+
.. versionadded:: 2.0
154+
"""
155+
# TODO: Do something like we did in self.commands for this
156+
return self.get_application_command
157+
158+
def get_application_command(
159+
self,
160+
name: str,
161+
guild_ids: Optional[List[int]] = None,
162+
type: Type[ApplicationCommand] = SlashCommand,
163+
) -> Optional[ApplicationCommand]:
164+
"""Get a :class:`.ApplicationCommand` from the internal list
165+
of commands.
166+
167+
.. versionadded:: 2.0
168+
169+
Parameters
170+
-----------
171+
name: :class:`str`
172+
The name of the command to get.
173+
guild_ids: List[:class:`int`]
174+
The guild ids associated to the command to get.
175+
type: Type[:class:`.ApplicationCommand`]
176+
The type of the command to get. Defaults to :class:`.SlashCommand`.
177+
178+
Returns
179+
--------
180+
Optional[:class:`.ApplicationCommand`]
181+
The command that was requested. If not found, returns ``None``.
182+
"""
183+
184+
for command in self._application_commands.values():
185+
if (
186+
command.name == name
187+
and isinstance(command, type)
188+
):
189+
if guild_ids is not None and command.guild_ids != guild_ids:
190+
return
191+
return command
140192

141193
async def sync_commands(self) -> None:
142194
"""|coro|
@@ -199,7 +251,7 @@ async def register_commands(self) -> None:
199251
type=i["type"],
200252
)
201253
cmd.id = i["id"]
202-
self.application_commands[cmd.id] = cmd
254+
self._application_commands[cmd.id] = cmd
203255

204256
# Permissions (Roles will be converted to IDs just before Upsert for Global Commands)
205257
global_permissions.append({"id": i["id"], "permissions": cmd.permissions})
@@ -234,7 +286,7 @@ async def register_commands(self) -> None:
234286
for i in cmds:
235287
cmd = find(lambda cmd: cmd.name == i["name"] and cmd.type == i["type"] and int(i["guild_id"]) in cmd.guild_ids, self.pending_application_commands)
236288
cmd.id = i["id"]
237-
self.application_commands[cmd.id] = cmd
289+
self._application_commands[cmd.id] = cmd
238290

239291
# Permissions
240292
permissions = [
@@ -325,7 +377,7 @@ async def register_commands(self) -> None:
325377
if len(new_cmd_perm["permissions"]) > 10:
326378
print(
327379
"Command '{name}' has more than 10 permission overrides in guild ({guild_id}).\nwill only use the first 10 permission overrides.".format(
328-
name=self.application_commands[new_cmd_perm["id"]].name,
380+
name=self._application_commands[new_cmd_perm["id"]].name,
329381
guild_id=guild_id,
330382
)
331383
)
@@ -375,12 +427,14 @@ async def process_application_commands(self, interaction: Interaction) -> None:
375427
return
376428

377429
try:
378-
command = self.application_commands[interaction.data["id"]]
430+
command = self._application_commands[interaction.data["id"]]
379431
except KeyError:
380432
self.dispatch("unknown_command", interaction)
381433
else:
382434
if interaction.type is InteractionType.auto_complete:
383-
return await command.invoke_autocomplete_callback(interaction)
435+
ctx = await self.get_autocomplete_context(interaction)
436+
ctx.command = command
437+
return await command.invoke_autocomplete_callback(ctx)
384438

385439
ctx = await self.get_application_context(interaction)
386440
ctx.command = command
@@ -516,6 +570,37 @@ class be provided, it must be similar enough to
516570
cls = ApplicationContext
517571
return cls(self, interaction)
518572

573+
async def get_autocomplete_context(
574+
self, interaction: Interaction, cls=None
575+
) -> AutocompleteContext:
576+
r"""|coro|
577+
578+
Returns the autocomplete context from the interaction.
579+
580+
This is a more low-level counter-part for :meth:`.process_application_commands`
581+
to allow users more fine grained control over the processing.
582+
583+
Parameters
584+
-----------
585+
interaction: :class:`discord.Interaction`
586+
The interaction to get the invocation context from.
587+
cls
588+
The factory class that will be used to create the context.
589+
By default, this is :class:`.AutocompleteContext`. Should a custom
590+
class be provided, it must be similar enough to
591+
:class:`.AutocompleteContext`\'s interface.
592+
593+
Returns
594+
--------
595+
:class:`.AutocompleteContext`
596+
The autocomplete context. The type of this can change via the
597+
``cls`` parameter.
598+
"""
599+
if cls is None:
600+
cls = AutocompleteContext
601+
return cls(self, interaction)
602+
603+
519604

520605
class BotBase(ApplicationCommandMixin, CogMixin):
521606
_supports_prefixed_commands = False

discord/client.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,7 +1196,7 @@ async def fetch_template(self, code: Union[Template, str]) -> Template:
11961196
data = await self.http.get_template(code)
11971197
return Template(data=data, state=self._connection) # type: ignore
11981198

1199-
async def fetch_guild(self, guild_id: int, /) -> Guild:
1199+
async def fetch_guild(self, guild_id: int, /, *, with_counts=True) -> Guild:
12001200
"""|coro|
12011201
12021202
Retrieves a :class:`.Guild` from an ID.
@@ -1215,6 +1215,12 @@ async def fetch_guild(self, guild_id: int, /) -> Guild:
12151215
guild_id: :class:`int`
12161216
The guild's ID to fetch from.
12171217
1218+
with_counts: :class:`bool`
1219+
Whether to include count information in the guild. This fills the
1220+
:attr:`.Guild.approximate_member_count` and :attr:`.Guild.approximate_presence_count`
1221+
fields.
1222+
1223+
.. versionadded:: 2.0
12181224
Raises
12191225
------
12201226
:exc:`.Forbidden`
@@ -1227,7 +1233,7 @@ async def fetch_guild(self, guild_id: int, /) -> Guild:
12271233
:class:`.Guild`
12281234
The guild from the ID.
12291235
"""
1230-
data = await self.http.get_guild(guild_id)
1236+
data = await self.http.get_guild(guild_id, with_counts = with_counts)
12311237
return Guild(data=data, state=self._connection)
12321238

12331239
async def create_guild(

discord/colour.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,31 @@ def nitro_pink(cls: Type[CT]) -> CT:
333333
.. versionadded:: 2.0
334334
"""
335335
return cls(0xf47fff)
336+
337+
@classmethod
338+
def embed_background(cls: Type[CT], theme: str = "dark") -> CT:
339+
"""A factory method that returns a :class:`Color` corresponding to the embed colors on discord clients, with a value of
340+
``0x2F3136`` (dark)
341+
``0xf2f3f5`` (light)
342+
``0x000000`` (amoled).
343+
344+
.. versionadded:: 2.0
345+
346+
Parameters
347+
-----------
348+
theme: :class:`str`
349+
The theme color to apply, must be one of "dark", "light", or "amoled".
350+
"""
351+
themes_cls = {
352+
"dark": 0x2F3136,
353+
"light": 0xf2f3f5,
354+
"amoled": 0x000000,
355+
}
356+
357+
if theme not in themes_cls:
358+
raise TypeError("Theme must be \"dark\", \"light\", or \"amoled\".")
359+
360+
return cls(themes_cls[theme])
336361

337362

338363
Color = Colour

discord/commands/commands.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def _parse_options(self, params) -> List[Option]:
450450
if p_obj.default != inspect.Parameter.empty:
451451
option.required = False
452452

453-
option.default = option.default or p_obj.default
453+
option.default = option.default if option.default is not None else p_obj.default
454454

455455
if option.default == inspect.Parameter.empty:
456456
option.default = None
@@ -532,7 +532,7 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
532532
else:
533533
await self.callback(ctx, **kwargs)
534534

535-
async def invoke_autocomplete_callback(self, interaction: Interaction):
535+
async def invoke_autocomplete_callback(self, ctx: AutocompleteContext):
536536
values = { i.name: i.default for i in self.options }
537537

538538
for op in interaction.data.get("options", []):
@@ -542,17 +542,25 @@ async def invoke_autocomplete_callback(self, interaction: Interaction):
542542
i["name"]:i["value"]
543543
for i in interaction.data["options"]
544544
})
545-
ctx = AutocompleteContext(interaction, command=self, focused=option, value=op.get("value"), options=values)
546-
if asyncio.iscoroutinefunction(option.autocomplete):
547-
result = await option.autocomplete(ctx)
545+
ctx.command = self
546+
ctx.focused = option
547+
ctx.value = op.get("value")
548+
ctx.options = values
549+
550+
if len(inspect.signature(option.autocomplete).parameters) == 2:
551+
instance = getattr(option.autocomplete, "__self__", ctx.cog)
552+
result = option.autocomplete(instance, ctx)
548553
else:
549554
result = option.autocomplete(ctx)
550555

556+
if asyncio.iscoroutinefunction(option.autocomplete):
557+
result = await result
558+
551559
choices = [
552560
o if isinstance(o, OptionChoice) else OptionChoice(o)
553561
for o in result
554562
][:25]
555-
return await interaction.response.send_autocomplete_result(choices=choices)
563+
return await ctx.interaction.response.send_autocomplete_result(choices=choices)
556564

557565

558566
def copy(self):
@@ -630,10 +638,11 @@ def __init__(
630638
for o in kwargs.pop("choices", list())
631639
]
632640
self.default = kwargs.pop("default", None)
641+
633642
if self.input_type == SlashCommandOptionType.integer:
634-
minmax_types = (int,)
643+
minmax_types = (int, type(None))
635644
elif self.input_type == SlashCommandOptionType.number:
636-
minmax_types = (int, float)
645+
minmax_types = (int, float, type(None))
637646
else:
638647
minmax_types = (type(None),)
639648
minmax_typehint = Optional[Union[minmax_types]] # type: ignore
@@ -790,11 +799,11 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
790799
ctx.interaction.data = option
791800
await command.invoke(ctx)
792801

793-
async def invoke_autocomplete_callback(self, interaction: Interaction) -> None:
794-
option = interaction.data["options"][0]
802+
async def invoke_autocomplete_callback(self, ctx: AutocompleteContext) -> None:
803+
option = ctx.interaction.data["options"][0]
795804
command = find(lambda x: x.name == option["name"], self.subcommands)
796-
interaction.data = option
797-
await command.invoke_autocomplete_callback(interaction)
805+
ctx.interaction.data = option
806+
await command.invoke_autocomplete_callback(ctx)
798807

799808

800809
class ContextMenuCommand(ApplicationCommand):
@@ -936,7 +945,7 @@ async def _invoke(self, ctx: ApplicationContext) -> None:
936945

937946
if self.cog is not None:
938947
await self.callback(self.cog, ctx, target)
939-
else:
948+
else:
940949
await self.callback(ctx, target)
941950

942951
def copy(self):

0 commit comments

Comments
 (0)