ayon-core/openpype/settings/entities/input_entities.py
2021-04-01 18:54:46 +02:00

513 lines
16 KiB
Python

import re
import copy
from abc import abstractmethod
from .base_entity import ItemEntity
from .lib import (
NOT_SET,
STRING_TYPE,
OverrideState
)
from .exceptions import (
DefaultsNotDefined,
StudioDefaultsNotDefined,
EntitySchemaError
)
from openpype.settings.constants import (
METADATA_KEYS,
M_ENVIRONMENT_KEY
)
class EndpointEntity(ItemEntity):
"""Entity that is a endpoint of settings value.
In most of cases endpoint entity does not have children entities and if has
then they are dynamic and can be removed/created. Is automatically set as
group if any parent is not, that is because of override metadata.
"""
def __init__(self, *args, **kwargs):
super(EndpointEntity, self).__init__(*args, **kwargs)
if (
not (self.group_item or self.is_group)
and not (self.is_dynamic_item or self.is_in_dynamic_item)
):
self.is_group = True
def schema_validations(self):
"""Validation of entity schema and schema hierarchy."""
# Default value when even defaults are not filled must be set
if self.value_on_not_set is NOT_SET:
reason = "Attribute `value_on_not_set` is not filled. {}".format(
self.__class__.__name__
)
raise EntitySchemaError(self, reason)
super(EndpointEntity, self).schema_validations()
@abstractmethod
def _settings_value(self):
pass
def settings_value(self):
if self._override_state is OverrideState.NOT_DEFINED:
return NOT_SET
if self.is_group:
if self._override_state is OverrideState.STUDIO:
if not self.has_studio_override:
return NOT_SET
elif self._override_state is OverrideState.PROJECT:
if not self.has_project_override:
return NOT_SET
return self._settings_value()
def on_change(self):
for callback in self.on_change_callbacks:
callback()
self.parent.on_child_change(self)
def update_default_value(self, value):
value = self._check_update_value(value, "default")
self._default_value = value
self.has_default_value = value is not NOT_SET
def update_studio_value(self, value):
value = self._check_update_value(value, "studio override")
self._studio_override_value = value
self.had_studio_override = bool(value is not NOT_SET)
def update_project_value(self, value):
value = self._check_update_value(value, "project override")
self._project_override_value = value
self.had_project_override = bool(value is not NOT_SET)
class InputEntity(EndpointEntity):
"""Endpoint entity without children."""
def __init__(self, *args, **kwargs):
super(InputEntity, self).__init__(*args, **kwargs)
self._value_is_modified = False
self._current_value = NOT_SET
def __eq__(self, other):
if isinstance(other, ItemEntity):
return self.value == other.value
return self.value == other
def get_child_path(self, child_obj):
raise TypeError("{} can't have children".format(
self.__class__.__name__
))
def schema_validations(self):
# Input entity must have file parent.
if not self.file_item:
raise EntitySchemaError(self, "Missing parent file entity.")
super(InputEntity, self).schema_validations()
@property
def value(self):
"""Entity's value without metadata."""
return self._current_value
def _settings_value(self):
return copy.deepcopy(self.value)
def set(self, value):
"""Change value."""
self._current_value = self.convert_to_valid_type(value)
self._on_value_change()
def _on_value_change(self):
# Change has_project_override attr value
if self._override_state is OverrideState.PROJECT:
self._has_project_override = True
elif self._override_state is OverrideState.STUDIO:
self._has_studio_override = True
self.on_change()
def on_change(self):
"""Callback triggered on change.
There are cases when this method may be called from other entity.
"""
value_is_modified = None
if self._override_state is OverrideState.PROJECT:
# Only value change
if (
self._has_project_override
and self._project_override_value is not NOT_SET
):
value_is_modified = (
self._current_value != self._project_override_value
)
if (
self._override_state is OverrideState.STUDIO
or value_is_modified is None
):
if (
self._has_studio_override
and self._studio_override_value is not NOT_SET
):
value_is_modified = (
self._current_value != self._studio_override_value
)
if value_is_modified is None:
value_is_modified = self._current_value != self._default_value
self._value_is_modified = value_is_modified
super(InputEntity, self).on_change()
def on_child_change(self, child_obj):
raise TypeError("Input entities do not contain children.")
@property
def has_unsaved_changes(self):
if self._override_state is OverrideState.NOT_DEFINED:
return False
if self._value_is_modified:
return True
# These may be stored on value change
if self._override_state is OverrideState.DEFAULTS:
if not self.has_default_value:
return True
elif self._override_state is OverrideState.STUDIO:
if self._has_studio_override != self.had_studio_override:
return True
if not self._has_studio_override and not self.has_default_value:
return True
elif self._override_state is OverrideState.PROJECT:
if self._has_project_override != self.had_project_override:
return True
if (
not self._has_project_override
and not self._has_studio_override
and not self.has_default_value
):
return True
return False
def set_override_state(self, state):
# Trigger override state change of root if is not same
if self.root_item.override_state is not state:
self.root_item.set_override_state(state)
return
self._override_state = state
# Ignore if is dynamic item and use default in that case
if not self.is_dynamic_item and not self.is_in_dynamic_item:
if state > OverrideState.DEFAULTS:
if not self.has_default_value:
raise DefaultsNotDefined(self)
elif state > OverrideState.STUDIO:
if not self.had_studio_override:
raise StudioDefaultsNotDefined(self)
if state is OverrideState.STUDIO:
self._has_studio_override = (
self._studio_override_value is not NOT_SET
)
elif state is OverrideState.PROJECT:
self._has_project_override = (
self._project_override_value is not NOT_SET
)
self._has_studio_override = self.had_studio_override
value = NOT_SET
if state is OverrideState.PROJECT:
value = self._project_override_value
if value is NOT_SET and state >= OverrideState.STUDIO:
value = self._studio_override_value
if value is NOT_SET and state >= OverrideState.DEFAULTS:
value = self._default_value
if value is NOT_SET:
value = self.value_on_not_set
self.has_default_value = False
else:
self.has_default_value = True
self._value_is_modified = False
self._current_value = copy.deepcopy(value)
def _discard_changes(self, on_change_trigger=None):
self._value_is_modified = False
if self._override_state >= OverrideState.PROJECT:
self._has_project_override = self.had_project_override
if self.had_project_override:
self._current_value = copy.deepcopy(
self._project_override_value
)
on_change_trigger.append(self.on_change)
return
if self._override_state >= OverrideState.STUDIO:
self._has_studio_override = self.had_studio_override
if self.had_studio_override:
self._current_value = copy.deepcopy(
self._studio_override_value
)
on_change_trigger.append(self.on_change)
return
if self._override_state >= OverrideState.DEFAULTS:
if self.has_default_value:
value = self._default_value
else:
value = self.value_on_not_set
self._current_value = copy.deepcopy(value)
on_change_trigger.append(self.on_change)
return
raise NotImplementedError("BUG: Unexcpected part of code.")
def _add_to_studio_default(self, _on_change_trigger):
self._has_studio_override = True
self.on_change()
def _remove_from_studio_default(self, on_change_trigger):
value = self._default_value
if value is NOT_SET:
value = self.value_on_not_set
self._current_value = copy.deepcopy(value)
self._has_studio_override = False
self._value_is_modified = False
on_change_trigger.append(self.on_change)
def _add_to_project_override(self, _on_change_trigger):
self._has_project_override = True
self.on_change()
def _remove_from_project_override(self, on_change_trigger):
if self._override_state is not OverrideState.PROJECT:
return
if not self._has_project_override:
return
self._has_project_override = False
if self._has_studio_override:
current_value = self._studio_override_value
elif self.has_default_value:
current_value = self._default_value
else:
current_value = self.value_on_not_set
self._current_value = copy.deepcopy(current_value)
on_change_trigger.append(self.on_change)
class NumberEntity(InputEntity):
schema_types = ["number"]
float_number_regex = re.compile(r"^\d+\.\d+$")
int_number_regex = re.compile(r"^\d+$")
def _item_initalization(self):
self.minimum = self.schema_data.get("minimum", -99999)
self.maximum = self.schema_data.get("maximum", 99999)
self.decimal = self.schema_data.get("decimal", 0)
value_on_not_set = self.schema_data.get("default", 0)
if self.decimal:
valid_value_types = (float, )
value_on_not_set = float(value_on_not_set)
else:
valid_value_types = (int, )
value_on_not_set = int(value_on_not_set)
self.valid_value_types = valid_value_types
self.value_on_not_set = value_on_not_set
def _convert_to_valid_type(self, value):
if isinstance(value, str):
new_value = None
if self.float_number_regex.match(value):
new_value = float(value)
elif self.int_number_regex.match(value):
new_value = int(value)
if new_value is not None:
self.log.info("{} - Converted str {} to {} {}".format(
self.path, value, type(new_value).__name__, new_value
))
value = new_value
if self.decimal:
if isinstance(value, float):
return value
if isinstance(value, int):
return float(value)
else:
if isinstance(value, int):
return value
if isinstance(value, float):
new_value = int(value)
if new_value != value:
self.log.info("{} - Converted float {} to int {}".format(
self.path, value, new_value
))
return new_value
return NOT_SET
class BoolEntity(InputEntity):
schema_types = ["boolean"]
def _item_initalization(self):
self.valid_value_types = (bool, )
self.value_on_not_set = True
class TextEntity(InputEntity):
schema_types = ["text"]
def _item_initalization(self):
self.valid_value_types = (STRING_TYPE, )
self.value_on_not_set = ""
# GUI attributes
self.multiline = self.schema_data.get("multiline", False)
self.placeholder_text = self.schema_data.get("placeholder")
def _convert_to_valid_type(self, value):
# Allow numbers converted to string
if isinstance(value, (int, float)):
return str(value)
return NOT_SET
class PathInput(InputEntity):
schema_types = ["path-input"]
def _item_initalization(self):
self.valid_value_types = (STRING_TYPE, )
self.value_on_not_set = ""
class RawJsonEntity(InputEntity):
schema_types = ["raw-json"]
def _item_initalization(self):
# Schema must define if valid value is dict or list
is_list = self.schema_data.get("is_list", False)
if is_list:
valid_value_types = (list, )
value_on_not_set = []
else:
valid_value_types = (dict, )
value_on_not_set = {}
self._is_list = is_list
self.valid_value_types = valid_value_types
self.value_on_not_set = value_on_not_set
self.default_metadata = {}
self.studio_override_metadata = {}
self.project_override_metadata = {}
@property
def is_list(self):
return self._is_list
@property
def is_dict(self):
return not self._is_list
def set(self, value):
new_value = self.convert_to_valid_type(value)
if isinstance(new_value, dict):
for key in METADATA_KEYS:
if key in new_value:
new_value.pop(key)
self._current_value = new_value
self._on_value_change()
@property
def metadata(self):
output = {}
if isinstance(self._current_value, dict) and self.is_env_group:
output[M_ENVIRONMENT_KEY] = {
self.env_group_key: list(self._current_value.keys())
}
return output
@property
def has_unsaved_changes(self):
result = super(RawJsonEntity, self).has_unsaved_changes
if not result:
result = self.metadata != self._metadata_for_current_state()
return result
def _metadata_for_current_state(self):
if (
self._override_state is OverrideState.PROJECT
and self._project_override_value is not NOT_SET
):
return self.project_override_metadata
if (
self._override_state >= OverrideState.STUDIO
and self._studio_override_value is not NOT_SET
):
return self.studio_override_metadata
return self.default_metadata
def _settings_value(self):
value = super(RawJsonEntity, self)._settings_value()
if self.is_env_group and isinstance(value, dict):
value.update(self.metadata)
return value
def _prepare_value(self, value):
metadata = {}
if isinstance(value, dict):
value = copy.deepcopy(value)
for key in METADATA_KEYS:
if key in value:
metadata[key] = value.pop(key)
return value, metadata
def update_default_value(self, value):
value = self._check_update_value(value, "default")
self.has_default_value = value is not NOT_SET
value, metadata = self._prepare_value(value)
self._default_value = value
self.default_metadata = metadata
def update_studio_value(self, value):
value = self._check_update_value(value, "studio override")
self.had_studio_override = value is not NOT_SET
value, metadata = self._prepare_value(value)
self._studio_override_value = value
self.studio_override_metadata = metadata
def update_project_value(self, value):
value = self._check_update_value(value, "project override")
self.had_project_override = value is not NOT_SET
value, metadata = self._prepare_value(value)
self._project_override_value = value
self.project_override_metadata = metadata