mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
736 lines
21 KiB
Python
736 lines
21 KiB
Python
import os
|
|
import json
|
|
import copy
|
|
import inspect
|
|
import logging
|
|
from abc import ABCMeta, abstractmethod, abstractproperty
|
|
|
|
import six
|
|
|
|
from .lib import (
|
|
NOT_SET,
|
|
gui_schema
|
|
)
|
|
from .constants import (
|
|
SYSTEM_SETTINGS_KEY,
|
|
WRAPPER_TYPES,
|
|
OverrideState
|
|
)
|
|
from pype.settings.lib import (
|
|
DEFAULTS_DIR,
|
|
|
|
get_default_settings,
|
|
|
|
get_studio_system_settings_overrides,
|
|
|
|
save_studio_settings,
|
|
|
|
find_environments,
|
|
DuplicatedEnvGroups
|
|
)
|
|
|
|
# from pype.api import Logger
|
|
class Logger:
|
|
def get_logger(self, name):
|
|
return logging.getLogger(name)
|
|
|
|
|
|
class InvalidValueType(Exception):
|
|
msg_template = "{}"
|
|
|
|
def __init__(self, valid_types, invalid_type, key):
|
|
msg = ""
|
|
if key:
|
|
msg += "Key \"{}\". ".format(key)
|
|
|
|
joined_types = ", ".join(
|
|
[str(valid_type) for valid_type in valid_types]
|
|
)
|
|
msg += "Got invalid type \"{}\". Expected: {}".format(
|
|
invalid_type, joined_types
|
|
)
|
|
self.msg = msg
|
|
super(InvalidValueType, self).__init__(msg)
|
|
|
|
|
|
@six.add_metaclass(ABCMeta)
|
|
class BaseEntity:
|
|
"""Partially abstract class for Setting's item type workflow."""
|
|
# `is_input_type` attribute says if has implemented item type methods
|
|
is_input_type = True
|
|
# Each input must have implemented default value for development
|
|
# when defaults are not filled yet.
|
|
default_input_value = NOT_SET
|
|
# Will allow to show actions for the item type (disabled for proxies) else
|
|
# item is skipped and try to trigger actions on it's parent.
|
|
allow_actions = True
|
|
# If item can store environment values
|
|
allow_to_environment = False
|
|
# Item will expand to full width in grid layout
|
|
expand_in_grid = False
|
|
|
|
def __init__(self, schema_data, parent, is_dynamic_item=False):
|
|
self.schema_data = schema_data
|
|
self.parent = parent
|
|
|
|
# Log object
|
|
self._log = None
|
|
|
|
# Item path attribute (may be filled or be dynamic)
|
|
self._path = None
|
|
|
|
# These should be set on initialization and not change then
|
|
self.valid_value_types = getattr(self, "valid_value_types", NOT_SET)
|
|
self.value_on_not_set = getattr(self, "value_on_not_set", NOT_SET)
|
|
|
|
self.is_group = False
|
|
self.is_file = False
|
|
self.group_item = None
|
|
self.file_item = None
|
|
self.root_item = None
|
|
|
|
# NOTE was `as_widget`
|
|
self.is_dynamic_item = is_dynamic_item
|
|
self.is_in_dynamic_item = False
|
|
|
|
self.env_group_key = None
|
|
self.is_env_group = False
|
|
|
|
self.roles = None
|
|
|
|
# Item require key to be able load or store data
|
|
self.require_key = True
|
|
|
|
self.key = None
|
|
self.label = None
|
|
|
|
# These attributes may change values during existence of an object
|
|
# Values
|
|
self.default_value = NOT_SET
|
|
self.studio_override_value = NOT_SET
|
|
self.project_override_value = NOT_SET
|
|
|
|
# Only for develop mode
|
|
self.defaults_not_set = False
|
|
|
|
# Default input attributes
|
|
self.has_default_value = False
|
|
|
|
self._has_studio_override = False
|
|
self.had_studio_override = False
|
|
|
|
self._has_project_override = False
|
|
self.had_project_override = False
|
|
|
|
self.value_is_modified = False
|
|
|
|
self.override_state = OverrideState.NOT_DEFINED
|
|
|
|
self.on_change_callbacks = []
|
|
|
|
@property
|
|
def has_studio_override(self):
|
|
if self.override_state >= OverrideState.STUDIO:
|
|
return self._has_studio_override
|
|
return False
|
|
|
|
@property
|
|
def has_project_override(self):
|
|
if self.override_state >= OverrideState.PROJECT:
|
|
return self._has_project_override
|
|
return False
|
|
|
|
@property
|
|
def path(self):
|
|
if self._path is not None:
|
|
return self._path
|
|
|
|
path = self.parent.get_child_path(self)
|
|
if not self.is_in_dynamic_item and not self.is_dynamic_item:
|
|
self._path = path
|
|
return path
|
|
|
|
@abstractmethod
|
|
def get_child_path(self, child_obj):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def schema_validations(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def set_override_state(self, state):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def on_value_change(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def on_change(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def on_child_change(self, child_obj):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def set_value(self, value):
|
|
pass
|
|
|
|
def validate_value(self, value):
|
|
if not self.valid_value_types:
|
|
return
|
|
|
|
for valid_type in self.valid_value_types:
|
|
if type(value) is valid_type:
|
|
return
|
|
|
|
key = getattr(self, "key", None)
|
|
raise InvalidValueType(self.valid_value_types, type(value), key)
|
|
|
|
def merge_metadata(self, current_metadata, new_metadata):
|
|
for key, value in new_metadata.items():
|
|
if key not in current_metadata:
|
|
current_metadata[key] = value
|
|
|
|
elif key == "groups":
|
|
current_metadata[key].extend(value)
|
|
|
|
elif key == "environments":
|
|
for group_key, subvalue in value.items():
|
|
if group_key not in current_metadata[key]:
|
|
current_metadata[key][group_key] = []
|
|
current_metadata[key][group_key].extend(subvalue)
|
|
|
|
else:
|
|
raise KeyError("Unknown metadata key: \"{}\"".format(key))
|
|
return current_metadata
|
|
|
|
def available_for_role(self, role_name=None):
|
|
if not self.roles:
|
|
return True
|
|
if role_name is None:
|
|
role_name = self.user_role
|
|
return role_name in self.roles
|
|
|
|
@property
|
|
def user_role(self):
|
|
"""Tool is running with any user role.
|
|
|
|
Returns:
|
|
str: user role as string.
|
|
|
|
"""
|
|
return self.parent.user_role
|
|
|
|
@property
|
|
def is_overidable(self):
|
|
""" care about overrides."""
|
|
return self.parent.is_overidable
|
|
|
|
@property
|
|
def log(self):
|
|
"""Auto created logger for debugging."""
|
|
if self._log is None:
|
|
self._log = Logger().get_logger(self.__class__.__name__)
|
|
return self._log
|
|
|
|
@abstractproperty
|
|
def value(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def update_default_value(self, parent_values):
|
|
"""Fill default values on startup or on refresh.
|
|
|
|
Default values stored in `pype` repository should update all items in
|
|
schema. Each item should take values for his key and set it's value or
|
|
pass values down to children items.
|
|
|
|
Args:
|
|
parent_values (dict): Values of parent's item. But in case item is
|
|
used as widget, `parent_values` contain value for item.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def update_studio_values(self, parent_values):
|
|
"""Fill studio override values on startup or on refresh.
|
|
|
|
Set studio value if is not set to NOT_SET, in that case studio
|
|
overrides are not set yet.
|
|
|
|
Args:
|
|
parent_values (dict): Values of parent's item. But in case item is
|
|
used as widget, `parent_values` contain value for item.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def update_project_values(self, parent_values):
|
|
"""Fill project override values on startup, refresh or project change.
|
|
|
|
Set project value if is not set to NOT_SET, in that case project
|
|
overrides are not set yet.
|
|
|
|
Args:
|
|
parent_values (dict): Values of parent's item. But in case item is
|
|
used as widget, `parent_values` contain value for item.
|
|
"""
|
|
pass
|
|
|
|
@abstractproperty
|
|
def schema_types(self):
|
|
pass
|
|
|
|
@abstractproperty
|
|
def has_unsaved_changes(self):
|
|
pass
|
|
|
|
@abstractproperty
|
|
def child_is_modified(self):
|
|
"""Any children item is modified."""
|
|
pass
|
|
|
|
@abstractproperty
|
|
def child_has_studio_override(self):
|
|
"""Any children item has studio overrides."""
|
|
pass
|
|
|
|
@abstractproperty
|
|
def child_has_project_override(self):
|
|
"""Any children item has project overrides."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def settings_value(self):
|
|
"""Value of an item without key."""
|
|
pass
|
|
|
|
def discard_changes(self, on_change_trigger=None):
|
|
initialized = False
|
|
if on_change_trigger is None:
|
|
initialized = True
|
|
on_change_trigger = []
|
|
|
|
self._discard_changes(on_change_trigger)
|
|
|
|
if initialized:
|
|
for callback in on_change_trigger:
|
|
callback()
|
|
|
|
@abstractmethod
|
|
def _discard_changes(self, on_change_trigger):
|
|
"""Item's implementation to discard all changes made by user.
|
|
|
|
Reset all values to same values as had when opened GUI
|
|
or when changed project.
|
|
|
|
Must not affect `had_studio_override` value or `was_overriden`
|
|
value. It must be marked that there are keys/values which are not in
|
|
defaults or overrides.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def set_studio_default(self):
|
|
"""Item's implementation to set current values as studio's overrides.
|
|
|
|
Mark item and it's children as they have studio overrides.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def reset_to_pype_default(self):
|
|
"""Item's implementation to remove studio overrides.
|
|
|
|
Mark item as it does not have studio overrides unset studio
|
|
override values.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def remove_overrides(self):
|
|
"""Item's implementation to remove project overrides.
|
|
|
|
Mark item as does not have project overrides. Must not change
|
|
`was_overriden` attribute value.
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def set_as_overriden(self):
|
|
"""Item's implementation to set values as overriden for project.
|
|
|
|
Mark item and all it's children as they're overriden. Must skip
|
|
items with children items that has attributes `is_group`
|
|
and `any_parent_is_group` set to False. In that case those items
|
|
are not meant to be overridable and should trigger the method on it's
|
|
children.
|
|
|
|
"""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def save(self):
|
|
"""Save data for current state."""
|
|
pass
|
|
|
|
|
|
class RootEntity(BaseEntity):
|
|
schema_types = ["root"]
|
|
|
|
# Root does not have to implement these
|
|
# TODO move them from BaseEntity to ItemEntity
|
|
update_default_value = None
|
|
update_studio_values = None
|
|
update_project_values = None
|
|
schema_validations = None
|
|
|
|
def __init__(self, schema_data):
|
|
super(RootEntity, self).__init__(schema_data, None, None)
|
|
self.root_item = self
|
|
self.item_initalization()
|
|
self.reset()
|
|
|
|
@abstractmethod
|
|
def reset(self):
|
|
pass
|
|
|
|
def __getitem__(self, key):
|
|
return self.non_gui_children[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
self.non_gui_children[key].set_value(value)
|
|
|
|
def __iter__(self):
|
|
for key in self.keys():
|
|
yield key
|
|
|
|
def get(self, key, default=None):
|
|
return self.non_gui_children.get(key, default)
|
|
|
|
def keys(self):
|
|
return self.non_gui_children.keys()
|
|
|
|
def values(self):
|
|
return self.non_gui_children.values()
|
|
|
|
def items(self):
|
|
return self.non_gui_children.items()
|
|
|
|
def _add_children(self, schema_data, first=True):
|
|
added_children = []
|
|
for children_schema in schema_data["children"]:
|
|
if children_schema["type"] in WRAPPER_TYPES:
|
|
_children_schema = copy.deepcopy(children_schema)
|
|
wrapper_children = self._add_children(
|
|
children_schema["children"]
|
|
)
|
|
_children_schema["children"] = wrapper_children
|
|
added_children.append(_children_schema)
|
|
continue
|
|
|
|
child_obj = self.create_schema_object(children_schema, self)
|
|
self.children.append(child_obj)
|
|
added_children.append(child_obj)
|
|
if type(child_obj) in self._gui_types:
|
|
continue
|
|
|
|
if child_obj.key in self.non_gui_children:
|
|
raise KeyError("Duplicated key \"{}\"".format(child_obj.key))
|
|
self.non_gui_children[child_obj.key] = child_obj
|
|
|
|
if not first:
|
|
return added_children
|
|
|
|
for child_obj in added_children:
|
|
if isinstance(child_obj, BaseEntity):
|
|
continue
|
|
self.gui_wrappers.append(child_obj)
|
|
|
|
def item_initalization(self):
|
|
self._loaded_types = None
|
|
self._gui_types = None
|
|
# Children are stored by key as keys are immutable and are defined by
|
|
# schema
|
|
self.valid_value_types = (dict, )
|
|
self.children = []
|
|
self.non_gui_children = {}
|
|
self.gui_wrappers = []
|
|
self._add_children(self.schema_data)
|
|
for children in self.children:
|
|
children.schema_validations()
|
|
|
|
def create_schema_object(self, schema_data, *args, **kwargs):
|
|
if self._loaded_types is None:
|
|
from pype.settings import entities
|
|
|
|
known_abstract_classes = (
|
|
BaseEntity,
|
|
entities.ItemEntity,
|
|
entities.InputEntity
|
|
)
|
|
|
|
self._loaded_types = {}
|
|
self._gui_types = []
|
|
for attr in dir(entities):
|
|
item = getattr(entities, attr)
|
|
if not inspect.isclass(item):
|
|
continue
|
|
|
|
if not issubclass(item, BaseEntity):
|
|
continue
|
|
|
|
if inspect.isabstract(item):
|
|
if item in known_abstract_classes:
|
|
continue
|
|
item()
|
|
|
|
for schema_type in item.schema_types:
|
|
self._loaded_types[schema_type] = item
|
|
|
|
gui_type = getattr(item, "gui_type", False)
|
|
if gui_type:
|
|
self._gui_types.append(item)
|
|
|
|
klass = self._loaded_types.get(schema_data["type"])
|
|
if not klass:
|
|
raise KeyError("Unknown type \"{}\"".format(schema_data["type"]))
|
|
return klass(schema_data, *args, **kwargs)
|
|
|
|
def set_override_state(self, state):
|
|
self.override_state = state
|
|
for child_obj in self.non_gui_children.values():
|
|
child_obj.set_override_state(state)
|
|
|
|
def set_value(self, value):
|
|
raise KeyError("{} does not allow to use `set_value`.".format(
|
|
self.__class__.__name__
|
|
))
|
|
|
|
def on_value_change(self):
|
|
raise TypeError("{} does not support `on_value_change`".format(
|
|
self.__class__.__name__
|
|
))
|
|
|
|
def on_change(self):
|
|
for callback in self.on_change_callbacks:
|
|
callback()
|
|
|
|
def on_child_change(self, child_obj):
|
|
self.on_change()
|
|
|
|
def get_child_path(self, child_obj):
|
|
for key, _child_obj in self.non_gui_children.items():
|
|
if _child_obj is child_obj:
|
|
return key
|
|
raise ValueError("Didn't found child {}".format(child_obj))
|
|
|
|
@property
|
|
def value(self):
|
|
output = {}
|
|
for key, child_obj in self.non_gui_children.items():
|
|
output[key] = child_obj.value
|
|
return output
|
|
|
|
def settings_value(self):
|
|
if self.override_state is OverrideState.NOT_DEFINED:
|
|
return NOT_SET
|
|
|
|
output = {}
|
|
for key, child_obj in self.non_gui_children.items():
|
|
value = child_obj.settings_value()
|
|
if self.override_state is OverrideState.DEFAULTS:
|
|
if value is not NOT_SET:
|
|
raise TypeError((
|
|
"Child returned NOT_SET on defaults settings. {}"
|
|
).format(child_obj.path))
|
|
|
|
for _key, _value in value.items():
|
|
new_key = "/".join([key, _key])
|
|
output[new_key] = _value
|
|
|
|
else:
|
|
if value is not NOT_SET:
|
|
output[key] = value
|
|
return output
|
|
|
|
@property
|
|
def child_has_studio_override(self):
|
|
if self.override_state >= OverrideState.STUDIO:
|
|
for child_obj in self.non_gui_children.values():
|
|
if child_obj.child_has_studio_override:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def child_has_project_override(self):
|
|
if self.override_state >= OverrideState.PROJECT:
|
|
for child_obj in self.non_gui_children.values():
|
|
if child_obj.child_has_project_override:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def has_unsaved_changes(self):
|
|
for child_obj in self.non_gui_children.values():
|
|
if child_obj.has_unsaved_changes:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def child_is_modified(self):
|
|
pass
|
|
|
|
def _discard_changes(self, on_change_trigger):
|
|
for child_obj in self.non_gui_children.values():
|
|
child_obj.discard_changes(on_change_trigger)
|
|
|
|
def remove_overrides(self):
|
|
for child_obj in self.non_gui_children.values():
|
|
child_obj.remove_overrides()
|
|
|
|
def reset_to_pype_default(self):
|
|
for child_obj in self.non_gui_children.values():
|
|
child_obj.reset_to_pype_default()
|
|
|
|
def set_as_overriden(self):
|
|
for child_obj in self.non_gui_children.values():
|
|
child_obj.set_as_overriden()
|
|
|
|
def set_studio_default(self):
|
|
for child_obj in self.non_gui_children.values():
|
|
child_obj.set_studio_default()
|
|
|
|
def save(self):
|
|
if self.override_state is OverrideState.NOT_DEFINED:
|
|
raise ValueError(
|
|
"Can't save if override state is set to NOT_DEFINED"
|
|
)
|
|
|
|
if not self.items_are_valid():
|
|
return
|
|
|
|
if self.override_state is OverrideState.DEFAULTS:
|
|
self.save_default_values()
|
|
|
|
elif self.override_state is OverrideState.STUDIO:
|
|
self.save_studio_values()
|
|
|
|
elif self.override_state is OverrideState.PROJECT:
|
|
self.save_project_values()
|
|
|
|
@abstractmethod
|
|
def items_are_valid(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def defaults_dir(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def validate_defaults_to_save(self, value):
|
|
pass
|
|
|
|
def save_default_values(self):
|
|
settings_value = self.settings_value()
|
|
if not self.validate_defaults_to_save(settings_value):
|
|
return
|
|
|
|
defaults_dir = self.defaults_dir()
|
|
for file_path, value in settings_value.items():
|
|
subpath = file_path + ".json"
|
|
|
|
output_path = os.path.join(defaults_dir, subpath)
|
|
dirpath = os.path.dirname(output_path)
|
|
if not os.path.exists(dirpath):
|
|
os.makedirs(dirpath)
|
|
|
|
print("Saving data to: ", subpath)
|
|
with open(output_path, "w") as file_stream:
|
|
json.dump(value, file_stream, indent=4)
|
|
|
|
@abstractmethod
|
|
def save_studio_values(self):
|
|
pass
|
|
|
|
@abstractmethod
|
|
def save_project_values(self):
|
|
pass
|
|
|
|
def set_defaults_state(self):
|
|
self.set_override_state(OverrideState.DEFAULTS)
|
|
|
|
def set_studio_state(self):
|
|
self.set_override_state(OverrideState.STUDIO)
|
|
|
|
def set_project_state(self):
|
|
self.set_override_state(OverrideState.PROJECT)
|
|
|
|
|
|
class SystemRootEntity(RootEntity):
|
|
def __init__(self, schema_data=None):
|
|
if schema_data is None:
|
|
schema_data = gui_schema("system_schema", "schema_main")
|
|
super(SystemRootEntity, self).__init__(schema_data)
|
|
|
|
def _reset_values(self):
|
|
default_value = get_default_settings()[SYSTEM_SETTINGS_KEY]
|
|
for key, child_obj in self.non_gui_children.items():
|
|
value = default_value.get(key, NOT_SET)
|
|
child_obj.update_default_value(value)
|
|
|
|
studio_overrides = {}
|
|
studio_overrides = get_studio_system_settings_overrides()
|
|
for key, child_obj in self.non_gui_children.items():
|
|
value = studio_overrides.get(key, NOT_SET)
|
|
child_obj.update_studio_values(value)
|
|
|
|
def reset(self, new_state=None):
|
|
if new_state is None:
|
|
new_state = self.override_state
|
|
|
|
if new_state is OverrideState.NOT_DEFINED:
|
|
new_state = OverrideState.DEFAULTS
|
|
|
|
if new_state is OverrideState.PROJECT:
|
|
raise ValueError("System settings can't store poject overrides.")
|
|
|
|
self._reset_values()
|
|
self.set_override_state(new_state)
|
|
|
|
def defaults_dir(self):
|
|
return os.path.join(DEFAULTS_DIR, SYSTEM_SETTINGS_KEY)
|
|
|
|
def items_are_valid(self):
|
|
# self.validate_duplicated_env_group()
|
|
return True
|
|
|
|
def save_studio_values(self):
|
|
# TODO add checks
|
|
settings_value = self.settings_value()
|
|
print(json.dumps(settings_value, indent=4))
|
|
# save_studio_settings(settings_value)
|
|
|
|
def save_project_values(self):
|
|
raise ValueError("System settings can't save project overrides.")
|
|
|
|
def validate_duplicated_env_group(self, values=None, overrides=None):
|
|
"""
|
|
Raises:
|
|
DuplicatedEnvGroups: When value contain duplicated env groups.
|
|
"""
|
|
# if overrides is not None:
|
|
# default_values = get_default_settings()[SYSTEM_SETTINGS_KEY]
|
|
# values = apply_overrides(default_values, overrides)
|
|
# else:
|
|
# values = copy.deepcopy(values)
|
|
#
|
|
# # Check if values contain duplicated environment groups
|
|
# find_environments(values)
|
|
pass
|
|
|
|
def validate_defaults_to_save(self, value):
|
|
return True
|