Files
sublime-music/sublime_music/ui/actions.py
Benjamin Schaaf 827636ade6 WIP
2022-01-09 00:03:38 +11:00

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()