mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
893 lines
24 KiB
Python
893 lines
24 KiB
Python
import os
|
|
import re
|
|
import collections
|
|
import uuid
|
|
import json
|
|
from abc import ABCMeta, abstractmethod, abstractproperty
|
|
|
|
import six
|
|
import clique
|
|
|
|
# Global variable which store attribude definitions by type
|
|
# - default types are registered on import
|
|
_attr_defs_by_type = {}
|
|
|
|
|
|
def register_attr_def_class(cls):
|
|
"""Register attribute definition.
|
|
|
|
Currently are registered definitions used to deserialize data to objects.
|
|
|
|
Attrs:
|
|
cls (AbtractAttrDef): Non-abstract class to be registered with unique
|
|
'type' attribute.
|
|
|
|
Raises:
|
|
KeyError: When type was already registered.
|
|
"""
|
|
|
|
if cls.type in _attr_defs_by_type:
|
|
raise KeyError("Type \"{}\" was already registered".format(cls.type))
|
|
_attr_defs_by_type[cls.type] = cls
|
|
|
|
|
|
def get_attributes_keys(attribute_definitions):
|
|
"""Collect keys from list of attribute definitions.
|
|
|
|
Args:
|
|
attribute_definitions (List[AbtractAttrDef]): Objects of attribute
|
|
definitions.
|
|
|
|
Returns:
|
|
Set[str]: Keys that will be created using passed attribute definitions.
|
|
"""
|
|
|
|
keys = set()
|
|
if not attribute_definitions:
|
|
return keys
|
|
|
|
for attribute_def in attribute_definitions:
|
|
if not isinstance(attribute_def, UIDef):
|
|
keys.add(attribute_def.key)
|
|
return keys
|
|
|
|
|
|
def get_default_values(attribute_definitions):
|
|
"""Receive default values for attribute definitions.
|
|
|
|
Args:
|
|
attribute_definitions (List[AbtractAttrDef]): Attribute definitions for
|
|
which default values should be collected.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Default values for passet attribute definitions.
|
|
"""
|
|
|
|
output = {}
|
|
if not attribute_definitions:
|
|
return output
|
|
|
|
for attr_def in attribute_definitions:
|
|
# Skip UI definitions
|
|
if not isinstance(attr_def, UIDef):
|
|
output[attr_def.key] = attr_def.default
|
|
return output
|
|
|
|
|
|
class AbstractAttrDefMeta(ABCMeta):
|
|
"""Meta class to validate existence of 'key' attribute.
|
|
|
|
Each object of `AbtractAttrDef` mus have defined 'key' attribute.
|
|
"""
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
obj = super(AbstractAttrDefMeta, self).__call__(*args, **kwargs)
|
|
init_class = getattr(obj, "__init__class__", None)
|
|
if init_class is not AbtractAttrDef:
|
|
raise TypeError("{} super was not called in __init__.".format(
|
|
type(obj)
|
|
))
|
|
return obj
|
|
|
|
|
|
@six.add_metaclass(AbstractAttrDefMeta)
|
|
class AbtractAttrDef(object):
|
|
"""Abstraction of attribute definiton.
|
|
|
|
Each attribute definition must have implemented validation and
|
|
conversion method.
|
|
|
|
Attribute definition should have ability to return "default" value. That
|
|
can be based on passed data into `__init__` so is not abstracted to
|
|
attribute.
|
|
|
|
QUESTION:
|
|
How to force to set `key` attribute?
|
|
|
|
Args:
|
|
key (str): Under which key will be attribute value stored.
|
|
default (Any): Default value of an attribute.
|
|
label (str): Attribute label.
|
|
tooltip (str): Attribute tooltip.
|
|
is_label_horizontal (bool): UI specific argument. Specify if label is
|
|
next to value input or ahead.
|
|
hidden (bool): Will be item hidden (for UI purposes).
|
|
disabled (bool): Item will be visible but disabled (for UI purposes).
|
|
"""
|
|
|
|
type_attributes = []
|
|
|
|
is_value_def = True
|
|
|
|
def __init__(
|
|
self,
|
|
key,
|
|
default,
|
|
label=None,
|
|
tooltip=None,
|
|
is_label_horizontal=None,
|
|
hidden=False,
|
|
disabled=False
|
|
):
|
|
if is_label_horizontal is None:
|
|
is_label_horizontal = True
|
|
|
|
if hidden is None:
|
|
hidden = False
|
|
|
|
self.key = key
|
|
self.label = label
|
|
self.tooltip = tooltip
|
|
self.default = default
|
|
self.is_label_horizontal = is_label_horizontal
|
|
self.hidden = hidden
|
|
self.disabled = disabled
|
|
self._id = uuid.uuid4().hex
|
|
|
|
self.__init__class__ = AbtractAttrDef
|
|
|
|
@property
|
|
def id(self):
|
|
return self._id
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, self.__class__):
|
|
return False
|
|
return self.key == other.key
|
|
|
|
@abstractproperty
|
|
def type(self):
|
|
"""Attribute definition type also used as identifier of class.
|
|
|
|
Returns:
|
|
str: Type of attribute definition.
|
|
"""
|
|
|
|
pass
|
|
|
|
@abstractmethod
|
|
def convert_value(self, value):
|
|
"""Convert value to a valid one.
|
|
|
|
Convert passed value to a valid type. Use default if value can't be
|
|
converted.
|
|
"""
|
|
|
|
pass
|
|
|
|
def serialize(self):
|
|
"""Serialize object to data so it's possible to recreate it.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Serialized object that can be passed to
|
|
'deserialize' method.
|
|
"""
|
|
|
|
data = {
|
|
"type": self.type,
|
|
"key": self.key,
|
|
"label": self.label,
|
|
"tooltip": self.tooltip,
|
|
"default": self.default,
|
|
"is_label_horizontal": self.is_label_horizontal,
|
|
"hidden": self.hidden,
|
|
"disabled": self.disabled
|
|
}
|
|
for attr in self.type_attributes:
|
|
data[attr] = getattr(self, attr)
|
|
return data
|
|
|
|
@classmethod
|
|
def deserialize(cls, data):
|
|
"""Recreate object from data.
|
|
|
|
Data can be received using 'serialize' method.
|
|
"""
|
|
|
|
return cls(**data)
|
|
|
|
|
|
# -----------------------------------------
|
|
# UI attribute definitoins won't hold value
|
|
# -----------------------------------------
|
|
|
|
class UIDef(AbtractAttrDef):
|
|
is_value_def = False
|
|
|
|
def __init__(self, key=None, default=None, *args, **kwargs):
|
|
super(UIDef, self).__init__(key, default, *args, **kwargs)
|
|
|
|
def convert_value(self, value):
|
|
return value
|
|
|
|
|
|
class UISeparatorDef(UIDef):
|
|
type = "separator"
|
|
|
|
|
|
class UILabelDef(UIDef):
|
|
type = "label"
|
|
|
|
def __init__(self, label):
|
|
super(UILabelDef, self).__init__(label=label)
|
|
|
|
|
|
# ---------------------------------------
|
|
# Attribute defintioins should hold value
|
|
# ---------------------------------------
|
|
|
|
class UnknownDef(AbtractAttrDef):
|
|
"""Definition is not known because definition is not available.
|
|
|
|
This attribute can be used to keep existing data unchanged but does not
|
|
have known definition of type.
|
|
"""
|
|
|
|
type = "unknown"
|
|
|
|
def __init__(self, key, default=None, **kwargs):
|
|
kwargs["default"] = default
|
|
super(UnknownDef, self).__init__(key, **kwargs)
|
|
|
|
def convert_value(self, value):
|
|
return value
|
|
|
|
|
|
class HiddenDef(AbtractAttrDef):
|
|
"""Hidden value of Any type.
|
|
|
|
This attribute can be used for UI purposes to pass values related
|
|
to other attributes (e.g. in multi-page UIs).
|
|
|
|
Keep in mind the value should be possible to parse by json parser.
|
|
"""
|
|
|
|
type = "hidden"
|
|
|
|
def __init__(self, key, default=None, **kwargs):
|
|
kwargs["default"] = default
|
|
kwargs["hidden"] = True
|
|
super(UnknownDef, self).__init__(key, **kwargs)
|
|
|
|
def convert_value(self, value):
|
|
return value
|
|
|
|
|
|
class NumberDef(AbtractAttrDef):
|
|
"""Number definition.
|
|
|
|
Number can have defined minimum/maximum value and decimal points. Value
|
|
is integer if decimals are 0.
|
|
|
|
Args:
|
|
minimum(int, float): Minimum possible value.
|
|
maximum(int, float): Maximum possible value.
|
|
decimals(int): Maximum decimal points of value.
|
|
default(int, float): Default value for conversion.
|
|
"""
|
|
|
|
type = "number"
|
|
type_attributes = [
|
|
"minimum",
|
|
"maximum",
|
|
"decimals"
|
|
]
|
|
|
|
def __init__(
|
|
self, key, minimum=None, maximum=None, decimals=None, default=None,
|
|
**kwargs
|
|
):
|
|
minimum = 0 if minimum is None else minimum
|
|
maximum = 999999 if maximum is None else maximum
|
|
# Swap min/max when are passed in opposited order
|
|
if minimum > maximum:
|
|
maximum, minimum = minimum, maximum
|
|
|
|
if default is None:
|
|
default = 0
|
|
|
|
elif not isinstance(default, (int, float)):
|
|
raise TypeError((
|
|
"'default' argument must be 'int' or 'float', not '{}'"
|
|
).format(type(default)))
|
|
|
|
# Fix default value by mim/max values
|
|
if default < minimum:
|
|
default = minimum
|
|
|
|
elif default > maximum:
|
|
default = maximum
|
|
|
|
super(NumberDef, self).__init__(key, default=default, **kwargs)
|
|
|
|
self.minimum = minimum
|
|
self.maximum = maximum
|
|
self.decimals = 0 if decimals is None else decimals
|
|
|
|
def __eq__(self, other):
|
|
if not super(NumberDef, self).__eq__(other):
|
|
return False
|
|
|
|
return (
|
|
self.decimals == other.decimals
|
|
and self.maximum == other.maximum
|
|
and self.maximum == other.maximum
|
|
)
|
|
|
|
def convert_value(self, value):
|
|
if isinstance(value, six.string_types):
|
|
try:
|
|
value = float(value)
|
|
except Exception:
|
|
pass
|
|
|
|
if not isinstance(value, (int, float)):
|
|
return self.default
|
|
|
|
if self.decimals == 0:
|
|
return int(value)
|
|
return round(float(value), self.decimals)
|
|
|
|
|
|
class TextDef(AbtractAttrDef):
|
|
"""Text definition.
|
|
|
|
Text can have multiline option so endline characters are allowed regex
|
|
validation can be applied placeholder for UI purposes and default value.
|
|
|
|
Regex validation is not part of attribute implemntentation.
|
|
|
|
Args:
|
|
multiline(bool): Text has single or multiline support.
|
|
regex(str, re.Pattern): Regex validation.
|
|
placeholder(str): UI placeholder for attribute.
|
|
default(str, None): Default value. Empty string used when not defined.
|
|
"""
|
|
|
|
type = "text"
|
|
type_attributes = [
|
|
"multiline",
|
|
"placeholder",
|
|
]
|
|
|
|
def __init__(
|
|
self, key, multiline=None, regex=None, placeholder=None, default=None,
|
|
**kwargs
|
|
):
|
|
if default is None:
|
|
default = ""
|
|
|
|
super(TextDef, self).__init__(key, default=default, **kwargs)
|
|
|
|
if multiline is None:
|
|
multiline = False
|
|
|
|
elif not isinstance(default, six.string_types):
|
|
raise TypeError((
|
|
"'default' argument must be a {}, not '{}'"
|
|
).format(six.string_types, type(default)))
|
|
|
|
if isinstance(regex, six.string_types):
|
|
regex = re.compile(regex)
|
|
|
|
self.multiline = multiline
|
|
self.placeholder = placeholder
|
|
self.regex = regex
|
|
|
|
def __eq__(self, other):
|
|
if not super(TextDef, self).__eq__(other):
|
|
return False
|
|
|
|
return (
|
|
self.multiline == other.multiline
|
|
and self.regex == other.regex
|
|
)
|
|
|
|
def convert_value(self, value):
|
|
if isinstance(value, six.string_types):
|
|
return value
|
|
return self.default
|
|
|
|
def serialize(self):
|
|
data = super(TextDef, self).serialize()
|
|
data["regex"] = self.regex.pattern
|
|
return data
|
|
|
|
|
|
class EnumDef(AbtractAttrDef):
|
|
"""Enumeration of single item from items.
|
|
|
|
Args:
|
|
items: Items definition that can be coverted to
|
|
`collections.OrderedDict`. Dictionary represent {value: label}
|
|
relation.
|
|
default: Default value. Must be one key(value) from passed items.
|
|
"""
|
|
|
|
type = "enum"
|
|
|
|
def __init__(self, key, items, default=None, **kwargs):
|
|
if not items:
|
|
raise ValueError((
|
|
"Empty 'items' value. {} must have"
|
|
" defined values on initialization."
|
|
).format(self.__class__.__name__))
|
|
|
|
items = collections.OrderedDict(items)
|
|
if default not in items:
|
|
for _key in items.keys():
|
|
default = _key
|
|
break
|
|
|
|
super(EnumDef, self).__init__(key, default=default, **kwargs)
|
|
|
|
self.items = items
|
|
|
|
def __eq__(self, other):
|
|
if not super(EnumDef, self).__eq__(other):
|
|
return False
|
|
|
|
if set(self.items.keys()) != set(other.items.keys()):
|
|
return False
|
|
|
|
for key, label in self.items.items():
|
|
if other.items[key] != label:
|
|
return False
|
|
return True
|
|
|
|
def convert_value(self, value):
|
|
if value in self.items:
|
|
return value
|
|
return self.default
|
|
|
|
def serialize(self):
|
|
data = super(TextDef, self).serialize()
|
|
data["items"] = list(self.items)
|
|
return data
|
|
|
|
|
|
class BoolDef(AbtractAttrDef):
|
|
"""Boolean representation.
|
|
|
|
Args:
|
|
default(bool): Default value. Set to `False` if not defined.
|
|
"""
|
|
|
|
type = "bool"
|
|
|
|
def __init__(self, key, default=None, **kwargs):
|
|
if default is None:
|
|
default = False
|
|
super(BoolDef, self).__init__(key, default=default, **kwargs)
|
|
|
|
def convert_value(self, value):
|
|
if isinstance(value, bool):
|
|
return value
|
|
return self.default
|
|
|
|
|
|
class FileDefItem(object):
|
|
def __init__(
|
|
self, directory, filenames, frames=None, template=None
|
|
):
|
|
self.directory = directory
|
|
|
|
self.filenames = []
|
|
self.is_sequence = False
|
|
self.template = None
|
|
self.frames = []
|
|
self.is_empty = True
|
|
|
|
self.set_filenames(filenames, frames, template)
|
|
|
|
def __str__(self):
|
|
return json.dumps(self.to_dict())
|
|
|
|
def __repr__(self):
|
|
if self.is_empty:
|
|
filename = "< empty >"
|
|
elif self.is_sequence:
|
|
filename = self.template
|
|
else:
|
|
filename = self.filenames[0]
|
|
|
|
return "<{}: \"{}\">".format(
|
|
self.__class__.__name__,
|
|
os.path.join(self.directory, filename)
|
|
)
|
|
|
|
@property
|
|
def label(self):
|
|
if self.is_empty:
|
|
return None
|
|
|
|
if not self.is_sequence:
|
|
return self.filenames[0]
|
|
|
|
frame_start = self.frames[0]
|
|
filename_template = os.path.basename(self.template)
|
|
if len(self.frames) == 1:
|
|
return "{} [{}]".format(filename_template, frame_start)
|
|
|
|
frame_end = self.frames[-1]
|
|
expected_len = (frame_end - frame_start) + 1
|
|
if expected_len == len(self.frames):
|
|
return "{} [{}-{}]".format(
|
|
filename_template, frame_start, frame_end
|
|
)
|
|
|
|
ranges = []
|
|
_frame_start = None
|
|
_frame_end = None
|
|
for frame in range(frame_start, frame_end + 1):
|
|
if frame not in self.frames:
|
|
add_to_ranges = _frame_start is not None
|
|
elif _frame_start is None:
|
|
_frame_start = _frame_end = frame
|
|
add_to_ranges = frame == frame_end
|
|
else:
|
|
_frame_end = frame
|
|
add_to_ranges = frame == frame_end
|
|
|
|
if add_to_ranges:
|
|
if _frame_start != _frame_end:
|
|
_range = "{}-{}".format(_frame_start, _frame_end)
|
|
else:
|
|
_range = str(_frame_start)
|
|
ranges.append(_range)
|
|
_frame_start = _frame_end = None
|
|
return "{} [{}]".format(
|
|
filename_template, ",".join(ranges)
|
|
)
|
|
|
|
def split_sequence(self):
|
|
if not self.is_sequence:
|
|
raise ValueError("Cannot split single file item")
|
|
|
|
paths = [
|
|
os.path.join(self.directory, filename)
|
|
for filename in self.filenames
|
|
]
|
|
return self.from_paths(paths, False)
|
|
|
|
@property
|
|
def ext(self):
|
|
if self.is_empty:
|
|
return None
|
|
_, ext = os.path.splitext(self.filenames[0])
|
|
if ext:
|
|
return ext
|
|
return None
|
|
|
|
@property
|
|
def lower_ext(self):
|
|
ext = self.ext
|
|
if ext is not None:
|
|
return ext.lower()
|
|
return ext
|
|
|
|
@property
|
|
def is_dir(self):
|
|
if self.is_empty:
|
|
return False
|
|
|
|
# QUESTION a better way how to define folder (in init argument?)
|
|
if self.ext:
|
|
return False
|
|
return True
|
|
|
|
def set_directory(self, directory):
|
|
self.directory = directory
|
|
|
|
def set_filenames(self, filenames, frames=None, template=None):
|
|
if frames is None:
|
|
frames = []
|
|
is_sequence = False
|
|
if frames:
|
|
is_sequence = True
|
|
|
|
if is_sequence and not template:
|
|
raise ValueError("Missing template for sequence")
|
|
|
|
self.is_empty = len(filenames) == 0
|
|
self.filenames = filenames
|
|
self.template = template
|
|
self.frames = frames
|
|
self.is_sequence = is_sequence
|
|
|
|
@classmethod
|
|
def create_empty_item(cls):
|
|
return cls("", "")
|
|
|
|
@classmethod
|
|
def from_value(cls, value, allow_sequences):
|
|
"""Convert passed value to FileDefItem objects.
|
|
|
|
Returns:
|
|
list: Created FileDefItem objects.
|
|
"""
|
|
|
|
# Convert single item to iterable
|
|
if not isinstance(value, (list, tuple, set)):
|
|
value = [value]
|
|
|
|
output = []
|
|
str_filepaths = []
|
|
for item in value:
|
|
if isinstance(item, dict):
|
|
item = cls.from_dict(item)
|
|
|
|
if isinstance(item, FileDefItem):
|
|
if not allow_sequences and item.is_sequence:
|
|
output.extend(item.split_sequence())
|
|
else:
|
|
output.append(item)
|
|
|
|
elif isinstance(item, six.string_types):
|
|
str_filepaths.append(item)
|
|
else:
|
|
raise TypeError(
|
|
"Unknown type \"{}\". Can't convert to {}".format(
|
|
str(type(item)), cls.__name__
|
|
)
|
|
)
|
|
|
|
if str_filepaths:
|
|
output.extend(cls.from_paths(str_filepaths, allow_sequences))
|
|
|
|
return output
|
|
|
|
@classmethod
|
|
def from_dict(cls, data):
|
|
return cls(
|
|
data["directory"],
|
|
data["filenames"],
|
|
data.get("frames"),
|
|
data.get("template")
|
|
)
|
|
|
|
@classmethod
|
|
def from_paths(cls, paths, allow_sequences):
|
|
filenames_by_dir = collections.defaultdict(list)
|
|
for path in paths:
|
|
normalized = os.path.normpath(path)
|
|
directory, filename = os.path.split(normalized)
|
|
filenames_by_dir[directory].append(filename)
|
|
|
|
output = []
|
|
for directory, filenames in filenames_by_dir.items():
|
|
if allow_sequences:
|
|
cols, remainders = clique.assemble(filenames)
|
|
else:
|
|
cols = []
|
|
remainders = filenames
|
|
|
|
for remainder in remainders:
|
|
output.append(cls(directory, [remainder]))
|
|
|
|
for col in cols:
|
|
frames = list(col.indexes)
|
|
paths = [filename for filename in col]
|
|
template = col.format("{head}{padding}{tail}")
|
|
|
|
output.append(cls(
|
|
directory, paths, frames, template
|
|
))
|
|
|
|
return output
|
|
|
|
def to_dict(self):
|
|
output = {
|
|
"is_sequence": self.is_sequence,
|
|
"directory": self.directory,
|
|
"filenames": list(self.filenames),
|
|
}
|
|
if self.is_sequence:
|
|
output.update({
|
|
"template": self.template,
|
|
"frames": list(sorted(self.frames)),
|
|
})
|
|
|
|
return output
|
|
|
|
|
|
class FileDef(AbtractAttrDef):
|
|
"""File definition.
|
|
It is possible to define filters of allowed file extensions and if supports
|
|
folders.
|
|
Args:
|
|
single_item(bool): Allow only single path item.
|
|
folders(bool): Allow folder paths.
|
|
extensions(List[str]): Allow files with extensions. Empty list will
|
|
allow all extensions and None will disable files completely.
|
|
extensions_label(str): Custom label shown instead of extensions in UI.
|
|
default(str, List[str]): Default value.
|
|
"""
|
|
|
|
type = "path"
|
|
type_attributes = [
|
|
"single_item",
|
|
"folders",
|
|
"extensions",
|
|
"allow_sequences",
|
|
"extensions_label",
|
|
]
|
|
|
|
def __init__(
|
|
self, key, single_item=True, folders=None, extensions=None,
|
|
allow_sequences=True, extensions_label=None, default=None, **kwargs
|
|
):
|
|
if folders is None and extensions is None:
|
|
folders = True
|
|
extensions = []
|
|
|
|
if default is None:
|
|
if single_item:
|
|
default = FileDefItem.create_empty_item().to_dict()
|
|
else:
|
|
default = []
|
|
else:
|
|
if single_item:
|
|
if isinstance(default, dict):
|
|
FileDefItem.from_dict(default)
|
|
|
|
elif isinstance(default, six.string_types):
|
|
default = FileDefItem.from_paths([default.strip()])[0]
|
|
|
|
else:
|
|
raise TypeError((
|
|
"'default' argument must be 'str' or 'dict' not '{}'"
|
|
).format(type(default)))
|
|
|
|
else:
|
|
if not isinstance(default, (tuple, list, set)):
|
|
raise TypeError((
|
|
"'default' argument must be 'list', 'tuple' or 'set'"
|
|
", not '{}'"
|
|
).format(type(default)))
|
|
|
|
# Change horizontal label
|
|
is_label_horizontal = kwargs.get("is_label_horizontal")
|
|
if is_label_horizontal is None:
|
|
kwargs["is_label_horizontal"] = False
|
|
|
|
self.single_item = single_item
|
|
self.folders = folders
|
|
self.extensions = set(extensions)
|
|
self.allow_sequences = allow_sequences
|
|
self.extensions_label = extensions_label
|
|
super(FileDef, self).__init__(key, default=default, **kwargs)
|
|
|
|
def __eq__(self, other):
|
|
if not super(FileDef, self).__eq__(other):
|
|
return False
|
|
|
|
return (
|
|
self.single_item == other.single_item
|
|
and self.folders == other.folders
|
|
and self.extensions == other.extensions
|
|
and self.allow_sequences == other.allow_sequences
|
|
)
|
|
|
|
def convert_value(self, value):
|
|
if isinstance(value, six.string_types) or isinstance(value, dict):
|
|
value = [value]
|
|
|
|
if isinstance(value, (tuple, list, set)):
|
|
string_paths = []
|
|
dict_items = []
|
|
for item in value:
|
|
if isinstance(item, six.string_types):
|
|
string_paths.append(item.strip())
|
|
elif isinstance(item, dict):
|
|
try:
|
|
FileDefItem.from_dict(item)
|
|
dict_items.append(item)
|
|
except (ValueError, KeyError):
|
|
pass
|
|
|
|
if string_paths:
|
|
file_items = FileDefItem.from_paths(string_paths)
|
|
dict_items.extend([
|
|
file_item.to_dict()
|
|
for file_item in file_items
|
|
])
|
|
|
|
if not self.single_item:
|
|
return dict_items
|
|
|
|
if not dict_items:
|
|
return self.default
|
|
return dict_items[0]
|
|
|
|
if self.single_item:
|
|
return FileDefItem.create_empty_item().to_dict()
|
|
return []
|
|
|
|
|
|
def serialize_attr_def(attr_def):
|
|
"""Serialize attribute definition to data.
|
|
|
|
Args:
|
|
attr_def (AbtractAttrDef): Attribute definition to serialize.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Serialized data.
|
|
"""
|
|
|
|
return attr_def.serialize()
|
|
|
|
|
|
def serialize_attr_defs(attr_defs):
|
|
"""Serialize attribute definitions to data.
|
|
|
|
Args:
|
|
attr_defs (List[AbtractAttrDef]): Attribute definitions to serialize.
|
|
|
|
Returns:
|
|
List[Dict[str, Any]]: Serialized data.
|
|
"""
|
|
|
|
return [
|
|
serialize_attr_def(attr_def)
|
|
for attr_def in attr_defs
|
|
]
|
|
|
|
|
|
def deserialize_attr_def(attr_def_data):
|
|
"""Deserialize attribute definition from data.
|
|
|
|
Args:
|
|
attr_def (Dict[str, Any]): Attribute definition data to deserialize.
|
|
"""
|
|
|
|
attr_type = attr_def_data.pop("type")
|
|
cls = _attr_defs_by_type[attr_type]
|
|
return cls.deserialize(attr_def_data)
|
|
|
|
|
|
def deserialize_attr_defs(attr_defs_data):
|
|
"""Deserialize attribute definitions.
|
|
|
|
Args:
|
|
List[Dict[str, Any]]: List of attribute definitions.
|
|
"""
|
|
|
|
return [
|
|
deserialize_attr_def(attr_def_data)
|
|
for attr_def_data in attr_defs_data
|
|
]
|
|
|
|
|
|
# Register attribute definitions
|
|
for _attr_class in (
|
|
UISeparatorDef,
|
|
UILabelDef,
|
|
UnknownDef,
|
|
NumberDef,
|
|
TextDef,
|
|
EnumDef,
|
|
BoolDef,
|
|
FileDef
|
|
):
|
|
register_attr_def_class(_attr_class)
|