diff --git a/libqtile/command/base.py b/libqtile/command/base.py index ee4bd04c15..52eee7afe6 100644 --- a/libqtile/command/base.py +++ b/libqtile/command/base.py @@ -28,8 +28,9 @@ import inspect import sys import traceback +import types from functools import partial -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, get_origin, get_args from libqtile.configurable import Configurable from libqtile.log_utils import logger @@ -74,7 +75,43 @@ def wrapper(func: Callable): func._mapping.append(name) # type:ignore else: logger.error("Unexpected value received in command decorator: %s", name) - return func + + def lift_arg(typ, arg): + # for stuff like int | None, allow either + if get_origin(typ) is types.UnionType: + for t in get_args(typ): + if t == types.NoneType: + # special case None? I don't know what this looks like + # coming over IPC + if arg == "": + return None + if arg == None: + return None + continue + + try: + return lift_arg(t, arg) + except TypeError: + pass + # uh oh, we couldn't lift it to anything + raise TypeError(f"{arg} is not a {typ}") + return typ(arg) + + def type_converter(*args, **kwargs): + converted_args = [] + converted_kwargs = dict() + + params = inspect.signature(func).parameters + + for arg, param in zip(args, params): + converted_args.append(lift_arg(param.annotation, arg)) + + for k, v in kwargs: + converted_kwargs[k] = lift_arg(params[k].annotation, v) + + return func(*converted_args, **converted_kwargs) + + return type_converter # If the decorator is added with no parentheses then we should treat it # as if it had been i.e. expose the decorated method