289 lines
9.2 KiB
Python
289 lines
9.2 KiB
Python
import inspect
|
|
import enum
|
|
import dataclasses
|
|
import pathlib
|
|
from typing import Callable, Optional, Tuple, Any, Union, List, Type
|
|
|
|
from gi.repository import Gio, GLib
|
|
|
|
|
|
NoneType = type(None)
|
|
|
|
|
|
def run_action(widget, name, *args):
|
|
print('run action', name, args)
|
|
|
|
group, action = name.split('.')
|
|
action_group = widget.get_action_group(group)
|
|
|
|
if args:
|
|
type_str = action_group.get_action_parameter_type(action)
|
|
assert type_str
|
|
|
|
if len(args) > 1:
|
|
param = _create_variant(type_str.dup_string(), tuple(args))
|
|
else:
|
|
param = _create_variant(type_str.dup_string(), args[0])
|
|
else:
|
|
param = None
|
|
|
|
action_group.activate_action(action, param)
|
|
|
|
|
|
def register_dataclass_actions(group, data, after=None):
|
|
fields = dataclasses.fields(type(data))
|
|
|
|
for field in fields:
|
|
if field.name[0] == '_':
|
|
continue
|
|
|
|
def set_field(value, name=field.name):
|
|
setattr(data, name, value)
|
|
if after:
|
|
after()
|
|
|
|
name = field.name.replace('_', '-')
|
|
try:
|
|
register_action(group, set_field, name=f'set-{name}', types=(field.type,))
|
|
except ValueError:
|
|
continue
|
|
|
|
|
|
def register_action(group, fn: Callable, name: Optional[str] = None, types: Tuple[Type] = None):
|
|
if name is None:
|
|
name = fn.__name__.replace('_', '-')
|
|
|
|
# Determine the type from the signature
|
|
if types is None:
|
|
signature = inspect.signature(fn)
|
|
types = tuple(p.annotation for p in signature.parameters.values())
|
|
|
|
if types:
|
|
if inspect.Parameter.empty in types:
|
|
raise ValueError('Missing parameter annotation for action ' + name)
|
|
|
|
has_multiple = len(types) > 1
|
|
if has_multiple:
|
|
param_type = Tuple.__getitem__(types)
|
|
else:
|
|
param_type = types[0]
|
|
|
|
type_str = variant_type_from_python(param_type)
|
|
var_type = GLib.VariantType(type_str)
|
|
|
|
build = generate_build_function(param_type)
|
|
if not build:
|
|
build = lambda a: a
|
|
else:
|
|
var_type = None
|
|
|
|
action = Gio.SimpleAction.new(name, var_type)
|
|
def activate(action, param):
|
|
if param is not None:
|
|
if has_multiple:
|
|
fn(*build(param.unpack()))
|
|
else:
|
|
fn(build(param.unpack()))
|
|
else:
|
|
fn()
|
|
action.connect('activate', activate)
|
|
|
|
if hasattr(group, 'add_action'):
|
|
group.add_action(action)
|
|
else:
|
|
group.insert(action)
|
|
|
|
|
|
def variant_type_from_python(py_type: type) -> str:
|
|
if py_type is bool:
|
|
return 'b'
|
|
elif py_type is int:
|
|
return 'x'
|
|
elif py_type is float:
|
|
return 'd'
|
|
elif py_type is str:
|
|
return 's'
|
|
elif py_type is Any:
|
|
return 'v'
|
|
elif isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
|
|
return 's'
|
|
elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
|
|
return variant_type_from_python(type(list(py_type)[0].value))
|
|
elif dataclasses.is_dataclass(py_type):
|
|
types = (f.type for f in dataclasses.fields(py_type))
|
|
|
|
return '(' + ''.join(map(variant_type_from_python, types)) + ')'
|
|
else:
|
|
origin = py_type.__origin__
|
|
|
|
if origin is list:
|
|
assert len(py_type.__args__) == 1
|
|
|
|
return 'a' + variant_type_from_python(py_type.__args__[0])
|
|
elif origin is tuple:
|
|
return '(' + ''.join(map(variant_type_from_python, py_type.__args__)) + ')'
|
|
elif origin is dict:
|
|
assert len(py_type.__args__) == 2
|
|
|
|
key = variant_type_from_python(py_type.__args__[0])
|
|
value = variant_type_from_python(py_type.__args__[1])
|
|
return 'a{' + key + value + '}'
|
|
elif origin is Union:
|
|
non_maybe = [t for t in py_type.__args__ if t is not NoneType]
|
|
has_maybe = len(non_maybe) != len(py_type.__args__)
|
|
|
|
if has_maybe and len(non_maybe) == 1:
|
|
return 'm' + variant_type_from_python(non_maybe[0])
|
|
|
|
return ('m[' if has_maybe else '[') + ''.join(''.join(map(variant_type_from_python, non_maybe))) + ']'
|
|
else:
|
|
raise ValueError('{} does not have an equivalent'.format(py_type))
|
|
|
|
|
|
def unbuilt_type(py_type: type) -> type:
|
|
if isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
|
|
return str
|
|
elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
|
|
return type(list(py_type)[0].value)
|
|
elif dataclasses.is_dataclass(py_type):
|
|
return tuple
|
|
return py_type
|
|
|
|
|
|
def generate_build_function(py_type: type) -> Optional[Callable]:
|
|
"""
|
|
Return a function for reconstructing dataclasses and enumerations after
|
|
unpacking a GVariant. When no reconstruction is needed None is returned.
|
|
"""
|
|
|
|
if isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath):
|
|
return py_type
|
|
elif isinstance(py_type, type) and issubclass(py_type, enum.Enum):
|
|
return py_type
|
|
elif dataclasses.is_dataclass(py_type):
|
|
types = tuple(f.type for f in dataclasses.fields(py_type))
|
|
tuple_build = generate_build_function(Tuple.__getitem__(types))
|
|
|
|
if not tuple_build:
|
|
return lambda values: py_type(*values)
|
|
|
|
return lambda values: py_type(*tuple_build(values))
|
|
elif hasattr(py_type, '__origin__'):
|
|
origin = py_type.__origin__
|
|
|
|
if origin is list:
|
|
assert len(py_type.__args__) == 1
|
|
|
|
build = generate_build_function(py_type.__args__[0])
|
|
|
|
if build:
|
|
return lambda values: [build(v) for v in values]
|
|
|
|
elif origin is tuple:
|
|
builds = list(map(generate_build_function, py_type.__args__))
|
|
|
|
if not any(builds):
|
|
return None
|
|
|
|
return lambda values: tuple((build(value) if build else value) for build, value in zip(builds, values))
|
|
elif origin is dict:
|
|
assert len(py_type.__args__) == 2
|
|
|
|
build_key = generate_build_function(py_type.__args__[0])
|
|
build_value = generate_build_function(py_type.__args__[1])
|
|
|
|
if not build_key and not build_value:
|
|
return None
|
|
|
|
return lambda values: {
|
|
(build_key(key) if build_key else key): (build_value(value) if build_value else value)
|
|
for key, value in values.items()}
|
|
elif origin is Union:
|
|
builds = list(map(generate_build_function, py_type.__args__))
|
|
|
|
if not any(builds):
|
|
return None
|
|
|
|
unbuilt_types = list(map(unbuilt_type, py_type.__args__))
|
|
|
|
def build(value):
|
|
for bld, type_ in zip(builds, unbuilt_types):
|
|
if isinstance(value, type_):
|
|
if bld:
|
|
return bld(value)
|
|
else:
|
|
return value
|
|
return value
|
|
return build
|
|
return None
|
|
|
|
|
|
_VARIANT_CONSTRUCTORS = {
|
|
'b': GLib.Variant.new_boolean,
|
|
'y': GLib.Variant.new_byte,
|
|
'n': GLib.Variant.new_int16,
|
|
'q': GLib.Variant.new_uint16,
|
|
'i': GLib.Variant.new_int32,
|
|
'u': GLib.Variant.new_uint32,
|
|
'x': GLib.Variant.new_int64,
|
|
't': GLib.Variant.new_uint64,
|
|
'h': GLib.Variant.new_handle,
|
|
'd': GLib.Variant.new_double,
|
|
's': GLib.Variant.new_string,
|
|
'o': GLib.Variant.new_object_path,
|
|
'g': GLib.Variant.new_signature,
|
|
'v': GLib.Variant.new_variant,
|
|
}
|
|
|
|
from gi._gi import variant_type_from_string
|
|
|
|
def _create_variant(type_str, value):
|
|
assert type_str
|
|
|
|
if isinstance(value, enum.Enum):
|
|
value = value.value
|
|
elif isinstance(value, pathlib.PurePath):
|
|
value = str(value)
|
|
elif dataclasses.is_dataclass(type(value)):
|
|
fields = dataclasses.fields(type(value))
|
|
value = tuple(getattr(value, field.name) for field in fields)
|
|
|
|
vtype = GLib.VariantType(type_str)
|
|
|
|
if type_str in _VARIANT_CONSTRUCTORS:
|
|
return _VARIANT_CONSTRUCTORS[type_str](value)
|
|
|
|
builder = GLib.VariantBuilder.new(vtype)
|
|
if value is None:
|
|
return builder.end()
|
|
|
|
if vtype.is_maybe():
|
|
builder.add_value(_create_variant(vtype.element().dup_string(), value))
|
|
return builder.end()
|
|
|
|
try:
|
|
iter(value)
|
|
except TypeError:
|
|
raise TypeError("Could not create array, tuple or dictionary entry from non iterable value %s %s" %
|
|
(type_str, value))
|
|
|
|
if vtype.is_tuple() and vtype.n_items() != len(value):
|
|
raise TypeError("Tuple mismatches value's number of elements %s %s" % (type_str, value))
|
|
if vtype.is_dict_entry() and len(value) != 2:
|
|
raise TypeError("Dictionary entries must have two elements %s %s" % (type_str, value))
|
|
|
|
if vtype.is_array():
|
|
element_type = vtype.element().dup_string()
|
|
if isinstance(value, dict):
|
|
value = value.items()
|
|
for i in value:
|
|
builder.add_value(_create_variant(element_type, i))
|
|
else:
|
|
remainer_format = type_str[1:]
|
|
for i in value:
|
|
dup = variant_type_from_string(remainer_format).dup_string()
|
|
builder.add_value(_create_variant(dup, i))
|
|
remainer_format = remainer_format[len(dup):]
|
|
|
|
return builder.end()
|