2424
2525from __future__ import annotations
2626
27- from typing import TYPE_CHECKING , Any
27+ from typing import TYPE_CHECKING , Any , Protocol , TypeVar , runtime_checkable
2828
29+ from twitchio .ext .commands .context import Context
2930from twitchio .user import User
31+ from twitchio .utils import Colour
3032
3133from .exceptions import *
3234
3335
3436if TYPE_CHECKING :
35- from .bot import Bot
3637 from .context import Context
37- from .types_ import BotT
3838
39- __all__ = ("_BaseConverter" ,)
39+
40+ __all__ = ("Converter" , "UserConverter" )
41+
4042
4143_BOOL_MAPPING : dict [str , bool ] = {
4244 "true" : True ,
5254}
5355
5456
55- class _BaseConverter :
56- def __init__ (self , client : Bot ) -> None :
57- self .__client : Bot = client
57+ T_co = TypeVar ("T_co" , covariant = True )
5858
59- self ._MAPPING : dict [Any , Any ] = {User : self ._user }
60- self ._DEFAULTS : dict [type , Any ] = {str : str , int : int , float : float , bool : self ._bool , type (None ): type (None )}
6159
62- def _bool (self , arg : str ) -> bool :
63- try :
64- result = _BOOL_MAPPING [arg .lower ()]
65- except KeyError :
66- pretty : str = " | " .join (f'"{ k } "' for k in _BOOL_MAPPING )
67- raise BadArgument (f'Failed to convert "{ arg } " to type bool. Expected any: [{ pretty } ]' , value = arg )
60+ @runtime_checkable
61+ class Converter (Protocol [T_co ]):
62+ """Base class used to create custom argument converters in :class:`~twitchio.ext.commands.Command`'s.
6863
69- return result
64+ To create a custom converter and do conversion logic on an argument you must override the :meth:`.convert` method.
65+ :meth:`.convert` must be a coroutine.
66+
67+ Examples
68+ --------
69+
70+ .. code:: python3
71+
72+ class LowerCaseConverter(commands.Converter[str]):
73+
74+ async def convert(self, ctx: commands.Context, arg: str) -> str:
75+ return arg.lower()
76+
77+
78+ @commands.command()
79+ async def test(ctx: commands.Context, arg: LowerCaseConverter) -> None: ...
80+
81+
82+ .. versionadded:: 3.1
83+ """
84+
85+ async def convert (self , ctx : Context [Any ], arg : str ) -> T_co :
86+ """|coro|
87+
88+ Method used on converters to implement conversion logic.
89+
90+ Parameters
91+ ----------
92+ ctx: :class:`~twitchio.ext.commands.Context`
93+ The context provided to the converter after command invocation has started.
94+ arg: str
95+ The argument received in raw form as a :class:`str` and passed to the converter to do conversion logic on.
96+ """
97+ raise NotImplementedError ("Classes that derive from Converter must implement this method." )
98+
99+
100+ class UserConverter (Converter [User ]):
101+ """The converter used to convert command arguments to a :class:`twitchio.User`.
102+
103+ This is a default converter which can be used in commands by annotating arguments with the :class:`twitchio.User` type.
104+
105+ .. note::
106+
107+ This converter uses an API call to attempt to fetch a valid :class:`twitchio.User`.
108+
109+
110+ Example
111+ -------
112+
113+ .. code:: python3
114+
115+ @commands.command()
116+ async def test(ctx: commands.Context, *, user: twitchio.User) -> None: ...
117+ """
118+
119+ async def convert (self , ctx : Context [Any ], arg : str ) -> User :
120+ client = ctx .bot
70121
71- async def _user (self , context : Context [BotT ], arg : str ) -> User :
72122 arg = arg .lower ()
73123 users : list [User ]
74124 msg : str = 'Failed to convert "{}" to User. A User with the ID or login could not be found.'
75125
76126 if arg .startswith ("@" ):
77127 arg = arg .removeprefix ("@" )
78- users = await self . __client .fetch_users (logins = [arg ])
128+ users = await client .fetch_users (logins = [arg ])
79129
80130 if not users :
81131 raise BadArgument (msg .format (arg ), value = arg )
82132
83133 if arg .isdigit ():
84- users = await self . __client .fetch_users (logins = [arg ], ids = [arg ])
134+ users = await client .fetch_users (logins = [arg ], ids = [arg ])
85135 else :
86- users = await self . __client .fetch_users (logins = [arg ])
136+ users = await client .fetch_users (logins = [arg ])
87137
88138 potential : list [User ] = []
89139
@@ -99,3 +149,66 @@ async def _user(self, context: Context[BotT], arg: str) -> User:
99149 return potential [0 ]
100150
101151 raise BadArgument (msg .format (arg ), value = arg )
152+
153+
154+ class ColourConverter (Converter [Colour ]):
155+ """The converter used to convert command arguments to a :class:`~twitchio.utils.Colour` object.
156+
157+ This is a default converter which can be used in commands by annotating arguments with the :class:`twitchio.utils.Colour` type.
158+
159+ This converter, attempts to convert ``hex`` and ``int`` type values only in the following formats:
160+
161+ - `"#FFDD00"`
162+ - `"FFDD00"`
163+ - `"0xFFDD00"`
164+ - `16768256`
165+
166+
167+ ``hex`` values are attempted first, followed by ``int``.
168+
169+ .. note::
170+
171+ There is an alias to this converter named ``ColorConverter``.
172+
173+ Example
174+ -------
175+
176+ .. code:: python3
177+
178+ @commands.command()
179+ async def test(ctx: commands.Context, *, colour: twitchio.utils.Colour) -> None: ...
180+
181+ .. versionadded:: 3.1
182+ """
183+
184+ async def convert (self , ctx : Context [Any ], arg : str ) -> Colour :
185+ try :
186+ result = Colour .from_hex (arg )
187+ except Exception :
188+ pass
189+ else :
190+ return result
191+
192+ try :
193+ result = Colour .from_int (int (arg ))
194+ except Exception :
195+ raise ConversionError (f"Unable to convert to Colour. { arg !r} is not a valid hex or colour integer value." )
196+
197+ return result
198+
199+
200+ ColorConverter = ColourConverter
201+
202+
203+ def _bool (arg : str ) -> bool :
204+ try :
205+ result = _BOOL_MAPPING [arg .lower ()]
206+ except KeyError :
207+ pretty : str = " | " .join (f'"{ k } "' for k in _BOOL_MAPPING )
208+ raise BadArgument (f'Failed to convert "{ arg } " to type bool. Expected any: [{ pretty } ]' , value = arg )
209+
210+ return result
211+
212+
213+ DEFAULT_CONVERTERS : dict [type , Any ] = {str : str , int : int , float : float , bool : _bool , type (None ): type (None )}
214+ CONVERTER_MAPPING : dict [Any , Converter [Any ] | type [Converter [Any ]]] = {User : UserConverter }
0 commit comments