From 042e045a3b7f4ed422a5b8bf40499dd349456883 Mon Sep 17 00:00:00 2001 From: Ben Simms Date: Mon, 3 Aug 2020 01:45:58 +0100 Subject: [PATCH 1/6] Fix typing.Literal converter (#14) * Add missing import to literal_converter * Actually fix typing.Literal * Fix error case * Make use of typing.get_origin --- discord/ext/alternatives/literal_converter.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/discord/ext/alternatives/literal_converter.py b/discord/ext/alternatives/literal_converter.py index b99a829..07fb8c0 100644 --- a/discord/ext/alternatives/literal_converter.py +++ b/discord/ext/alternatives/literal_converter.py @@ -1,23 +1,26 @@ -from typing import Literal, get_args -from discord.ext.commands.errors import ConversionError +from typing import Literal, get_args, get_origin +from discord.ext.commands import Command +from discord.ext.commands.errors import ConversionError, BadArgument _old_actual_conversion = Command._actual_conversion -def _actual_conversion(self, ctx, converter, argument, param): - if converter is Literal: +async def _actual_conversion(self, ctx, converter, argument, param): + origin = get_origin(converter) + + if origin is Literal: items = get_args(converter) if all(i for i in items if isinstance(i, str)): if argument in items: return argument - raise commands.BadArgument(f"Expected literal: one of {list(map(repr, self.literals))}") + raise BadArgument(f"Expected literal: one of {list(map(repr, items))}") elif all(i for i in items if not isinstance(i, str)): - ret = _old_actual_conversion(self, ctx, type(items[0]), argument, param) + ret = await _old_actual_conversion(self, ctx, type(items[0]), argument, param) return ret in items else: raise ConversionError('Literal contains multiple conflicting types.') - return _old_actual_conversion(self, ctx, converter, argument, param) + return await _old_actual_conversion(self, ctx, converter, argument, param) Command._actual_conversion = _actual_conversion From f74735728d253692bf7c7c0f26ba58f07df57a49 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 3 Aug 2020 11:10:29 +1000 Subject: [PATCH 2/6] Add py_allow utility function to validate minimum python version --- discord/ext/alternatives/_alternative_converters.py | 3 --- discord/ext/alternatives/_common.py | 11 +++++++++++ discord/ext/alternatives/asset_converter.py | 2 +- discord/ext/alternatives/converter_dict.py | 2 +- discord/ext/alternatives/guild_converter.py | 2 +- setup.py | 2 +- 6 files changed, 15 insertions(+), 7 deletions(-) delete mode 100644 discord/ext/alternatives/_alternative_converters.py create mode 100644 discord/ext/alternatives/_common.py diff --git a/discord/ext/alternatives/_alternative_converters.py b/discord/ext/alternatives/_alternative_converters.py deleted file mode 100644 index 469624d..0000000 --- a/discord/ext/alternatives/_alternative_converters.py +++ /dev/null @@ -1,3 +0,0 @@ -_ALL = { - # This will be populated by loaded alternative converters at runtime -} diff --git a/discord/ext/alternatives/_common.py b/discord/ext/alternatives/_common.py new file mode 100644 index 0000000..bf6bbee --- /dev/null +++ b/discord/ext/alternatives/_common.py @@ -0,0 +1,11 @@ +import sys + +from typing import Optional, NoReturn + +_ALL = { + # This will be populated by loaded alternative converters at runtime +} + +def py_allow(major: int, minor: int, micro: int) -> Optional[NoReturn]: + if sys.version_info < (major, minor, micro): + raise RuntimeError('the version of Python installed is not compatible with this experiment.') diff --git a/discord/ext/alternatives/asset_converter.py b/discord/ext/alternatives/asset_converter.py index 033fa42..27aa7e2 100644 --- a/discord/ext/alternatives/asset_converter.py +++ b/discord/ext/alternatives/asset_converter.py @@ -22,7 +22,7 @@ async def test(ctx, image: Asset): from discord import Asset import typing -from ._alternative_converters import _ALL +from ._common import _ALL # Basic Asset Converter diff --git a/discord/ext/alternatives/converter_dict.py b/discord/ext/alternatives/converter_dict.py index 5e35b3b..520956c 100644 --- a/discord/ext/alternatives/converter_dict.py +++ b/discord/ext/alternatives/converter_dict.py @@ -23,7 +23,7 @@ async def yert(ctx, yert: Yert): import discord from discord.ext.commands import Bot, converter, Command -from ._alternative_converters import _ALL +from ._common import _ALL _BUILTINS = ( bool, diff --git a/discord/ext/alternatives/guild_converter.py b/discord/ext/alternatives/guild_converter.py index a82529c..1887de5 100644 --- a/discord/ext/alternatives/guild_converter.py +++ b/discord/ext/alternatives/guild_converter.py @@ -12,7 +12,7 @@ async def test(ctx, server: Guild): from discord.ext.commands import BadArgument, converter, Context from discord import Guild, utils -from ._alternative_converters import _ALL +from ._common import _ALL # Basic Guild Converter diff --git a/setup.py b/setup.py index 051c324..1d97ae2 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ setup( name="discord-ext-alternatives", author="NCPlayz", - python_requires=">=3.6.0", + python_requires=">=3.5.3", url="https://github.com/Ext-Creators/discord-ext-alternatives", version=version, packages=["discord.ext.alternatives"], From e982bd779806a525d0f10842371406ac43a7b692 Mon Sep 17 00:00:00 2001 From: NCPlayz Date: Mon, 3 Aug 2020 23:43:48 +0100 Subject: [PATCH 3/6] [bug] fix NameError --- discord/ext/alternatives/asset_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ext/alternatives/asset_converter.py b/discord/ext/alternatives/asset_converter.py index 27aa7e2..22d4c36 100644 --- a/discord/ext/alternatives/asset_converter.py +++ b/discord/ext/alternatives/asset_converter.py @@ -19,7 +19,7 @@ async def test(ctx, image: Asset): from discord.ext.commands import converter, Context, errors, Command from inspect import Parameter -from discord import Asset +from discord import Asset, DiscordException import typing from ._common import _ALL From 5482a4e7a5387563fe1662ba95e22134572a369c Mon Sep 17 00:00:00 2001 From: Sebastian Law <44045823+SebbyLaw@users.noreply.github.com> Date: Sun, 13 Sep 2020 14:03:56 -0700 Subject: [PATCH 4/6] fix bound method call to pass self (#17) --- discord/ext/alternatives/silent_delete.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/ext/alternatives/silent_delete.py b/discord/ext/alternatives/silent_delete.py index dc9be0d..771b4ae 100644 --- a/discord/ext/alternatives/silent_delete.py +++ b/discord/ext/alternatives/silent_delete.py @@ -11,7 +11,7 @@ async def delete(self, *, delay=None, silent=False): try: - await _old_delete(delay=delay) + await _old_delete(self, delay=delay) except Exception: if not silent: raise From b86e996dc902c6ad7b7e6b96d0f788c5e2e61a43 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 6 Oct 2020 01:37:29 +1000 Subject: [PATCH 5/6] Fix issue with converter_dict and typing.Union (#22) --- discord/ext/alternatives/converter_dict.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/discord/ext/alternatives/converter_dict.py b/discord/ext/alternatives/converter_dict.py index 520956c..2586d41 100644 --- a/discord/ext/alternatives/converter_dict.py +++ b/discord/ext/alternatives/converter_dict.py @@ -73,15 +73,10 @@ def set(self, k, v): Bot.converters = _GLOBAL_CONVERTER_DICT -def _get_converter(self, param): - obj = param.annotation - if obj is param.empty: - if param.default is not param.empty: - converter = _GLOBAL_CONVERTER_DICT.get(type(param.default), str) - else: - converter = str - else: - converter = _GLOBAL_CONVERTER_DICT.get(obj, obj) # fall back on its typehint if its converter isn't registered - return converter - -Command._get_converter = _get_converter +_old_actual_conversion = Command._actual_conversion + +async def _actual_conversion(self, ctx, converter, argument, param): + converter = _GLOBAL_CONVERTER_DICT.get(converter, converter) + return await _old_actual_conversion(self, ctx, converter, argument, param) + +Command._actual_conversion = _actual_conversion From 5ff36dfd030adad851beed42a3aea26ed28cc53a Mon Sep 17 00:00:00 2001 From: Nadir Chowdhury Date: Sun, 11 Oct 2020 19:24:35 +0100 Subject: [PATCH 6/6] Implement binary operators for command checks. (#23) * Implement binary operators for command checks. * need to set this for some reason too.. --- discord/ext/alternatives/binary_checks.py | 184 ++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 discord/ext/alternatives/binary_checks.py diff --git a/discord/ext/alternatives/binary_checks.py b/discord/ext/alternatives/binary_checks.py new file mode 100644 index 0000000..b0faa17 --- /dev/null +++ b/discord/ext/alternatives/binary_checks.py @@ -0,0 +1,184 @@ +"""An experiment that allows you to use the `&` and `|` operators +on checks, allowing for easier control over check conditions. + +This should not break pre-existing checks. + +Examples: +```py +@guild_only() | dm_only() +@b.command() +async def there(ctx): + await ctx.send('is literally no reason to use both the guild_only and dm_only checks on the same command.') +``` + +```py +@dm_only() | is_owner() +@bot.command() +async def hello(ctx): + await ctx.send('world!') +``` + +```py +@guild_only() & ((has_role('Capitalist') & bot_has_role('Leader of the Communist Revolution') ) | !is_owner()) +@bot.command() +async def no(ctx): + await ctx.send('stop this') +``` +""" +from discord.ext import commands +import inspect + + +class CheckDecorator: + def __init__(self, predicate): + self.check = Only(Check(predicate)) + + def __call__(self, func): + if isinstance(func, commands.Command): + func.checks.append(self.check) + else: + if not hasattr(func, '__commands_checks__'): + func.__commands_checks__ = [] + + func.__commands_checks__.append(self.check) + + return func + + def __repr__(self): + return f'CheckDecorator' + + def __invert__(self): + ~self.check.first + return self.check + + def __or__(self, other): + self.check.first = Either(self.check.first, other.check.first if isinstance(other, CheckDecorator) else other) + return self.check + + def __and__(self, other): + self.check.first = Both(self.check.first, other.check.first if isinstance(other, CheckDecorator) else other) + return self.check + + +class Check: + def __init__(self, predicate): + self.predicate = predicate + self.inverted = False + + def __repr__(self): + return f'Check(predicate={self.predicate!r}, inverted={self.inverted})' + + async def __call__(self, *args, **kwargs): + r = self.predicate(*args, **kwargs) + if isinstance(r, bool): + r = r + else: + r = await r + + if self.inverted: + r = not r + + return r + + def __invert__(self): + self.inverted = not self.inverted + return self + + def __or__(self, other): + return Either(self.predicate, other if isinstance(other, CheckOp) else other.predicate) + + def __and__(self, other): + return Both(self.predicate, other if isinstance(other, CheckOp) else other.predicate) + +commands.core.check = CheckDecorator +commands.check = CheckDecorator + +class Only: + def __init__(self, first: Check): + self.first = first + self.inverted = False + + def _call(self, *args, **kwargs): + return self.first(*args, **kwargs) + + def __call__(self, *args, **kwargs): + if isinstance(args[0], commands.Command): + args[0].checks.append(self) + return args[0] + else: + return self._call(*args, **kwargs) + + def __repr__(self): + return f'Only(first={self.first!r})' + + def __invert__(self): + self.inverted = not self.inverted + return self + + def __or__(self, other): + return Either(self, other) + + def __and__(self, other): + return Both(self, other) + + +class CheckOp: + def __init__(self, first: Check, second: Check): + self.first = first + self.second = second + self.inverted = False + self.check = self + + def __repr__(self): + return f'{self.__class__.__name__}(first={self.first!r}, second={self.second!r}, inverted={self.inverted})' + + async def _try_single(self, callback, *args, **kwargs): + r = await callback(*args, **kwargs) + + return not r if self.inverted else r + + async def _try_call(self, *args, **kwargs): + return await self._try_single(self.first, *args, **kwargs), await self._try_single(self.second, *args, **kwargs) + + def __invert__(self): + self.inverted = not self.inverted + return self + + def __or__(self, other): + return Either(self, other) + + def __and__(self, other): + return Both(self, other) + + async def _call(self, *args, **kwargs): + ... + + def __call__(self, *args, **kwargs): + if isinstance(args[0], commands.Command): + args[0].checks.append(self) + return args[0] + else: + return self._call(*args, **kwargs) + + +class Both(CheckOp): + async def _call(self, *args, **kwargs): + fs, ss = await self._try_call(*args, **kwargs) + return fs and ss + + +class Either(CheckOp): + async def _call(self, *args, **kwargs): + fs = False + try: + fs = await self._try_single(self.first, *args, **kwargs) + except: + pass + + ss = False + try: + ss = await self._try_single(self.second, *args, **kwargs) + except: + pass + + return fs or ss