问题
While I was coding for help command for my bot in Discord.py, This function was not working as I expected.
def syntax(command):
cmd_and_aliases = '|'.join([str(command), *command.aliases])
params = []
for key, value in command.params.items():
if key not in ('self', 'ctx'):
params.append(f'[{key}]' if 'NoneType' in str(value) else f'<{key}>')
params = ' '.join(params)
return f'`{cmd_and_aliases} {params}`'
What I wanted for this function was, returning and printing names and aliases to call up an discord command and what parts are necessary and what parts are not to run that command properly. As the Optional[str] in Typing can be translated as Union[str, None], What I wanted to get was what their could be and if it can be None, then it will be appended to the params list wrappend in square bracket, or else in angle bracket.
`name|aliases <necessary part> [optional part]`
This is what the function would originally would have returned, but both the necessary part and optional part are shown as wrapped in angle brackets.
@command(name='slap', aliases=['hit'])
async def slap_member(self, ctx, member: Member, *, reason: Optional[str] = 'for no reason'):
For example, for this command the result would have been like below.
slap|hit <Member> [reason]
But what I ended was getting was:
slap|hit <Member> <reason>.
Is there something I have done wrong or is there any update that would have made this function broken? If there is something I did wrong, please help me by showing what I can do otherwise.
回答1:
The "Why"
This behavior has indeed changed in 3.9; I believe this commit specifically is the cause. It looks like these changes begun in this commit, but I believe the manifestation of your particular issue is due to the former.
I'm not deeply familiar with the plumbing of the typing
module, but from what I can gather, the representation of Optional
is now special-cased and returns itself as-is, i.e. typing.Optional[x]
instead of "expanding" to typing.Union[x, NoneType]
. They still have the same "type", however, but the important bit here is that their "type" has changed to _UnionGenericAlias, whose __repr__
is:
def __repr__(self):
args = self.__args__
if len(args) == 2:
if args[0] is type(None):
return f'typing.Optional[{_type_repr(args[1])}]'
elif args[1] is type(None):
return f'typing.Optional[{_type_repr(args[0])}]'
return super().__repr__()
Possible Solutions
First, I'd like to point out that you can use command.clean_params instead of command.params
(which strangely isn't documented, from what I can tell). It functions the same, except it automatically excludes self
and ctx
so you don't have to check for it. I'll be using it in my solutions below.
I came up with two possible solutions. The first is quite close to what you have, in that we do a simple string check for typing.Optional
:
for key, value in command.clean_params.items():
optional = str(value).startswith('typing.Optional')
params.append(f'[{key}]' if optional else f'<{key}>')
Now I personally am not fond of string checks when it comes to determining types (Generics in this case), so I would opt for something a bit different. As described in PEP 585, we can do runtime introspection on Generics, so let's take advantage of that.
for key, value in command.clean_params.items():
optional = value.__origin__ is Union and value.__args__[1] is type(None)
params.append(f'[{key}]' if optional else f'<{key}>')
Another benefit of this approach is that it will work in Python 3.8 (and I think 3.7 as well), whereas the string checking version will obviously only work in 3.9. You could leverage sys.version_info
and have multiple string checks, but that seems like too much hassle in comparison.
来源:https://stackoverflow.com/questions/65771191/is-there-an-update-on-typing-optional-on-python-3-9-or-am-i-doing-something-wron