import copy import collections from .lib import ( WRAPPER_TYPES, OverrideState, NOT_SET ) from openpype.settings.constants import ( METADATA_KEYS, M_OVERRIDEN_KEY, KEY_REGEX ) from . import ( BaseItemEntity, ItemEntity, BoolEntity, GUIEntity ) from .exceptions import ( SchemaDuplicatedKeys, EntitySchemaError, InvalidKeySymbols ) example_schema = { "type": "dict-conditional", "key": "KEY", "label": "LABEL", "enum_key": "type", "enum_label": "label", "enum_children": [ { "key": "action", "label": "Action", "children": [ { "type": "text", "key": "key", "label": "Key" }, { "type": "text", "key": "label", "label": "Label" }, { "type": "text", "key": "command", "label": "Comand" } ] }, { "key": "menu", "label": "Menu", "children": [ { "type": "list", "object_type": "text" } ] }, { "key": "separator", "label": "Separator" } ] } class DictConditionalEntity(ItemEntity): schema_types = ["dict-conditional"] _default_label_wrap = { "use_label_wrap": False, "collapsible": False, "collapsed": True } def __getitem__(self, key): """Return entity inder key.""" return self.non_gui_children[self.current_enum][key] def __setitem__(self, key, value): """Set value of item under key.""" child_obj = self.non_gui_children[self.current_enum][key] child_obj.set(value) def __iter__(self): """Iter through keys.""" for key in self.keys(): yield key def __contains__(self, key): """Check if key is available.""" return key in self.non_gui_children[self.current_enum] def get(self, key, default=None): """Safe entity getter by key.""" return self.non_gui_children[self.current_enum].get(key, default) def keys(self): """Entity's keys.""" keys = list(self.non_gui_children[self.current_enum].keys()) keys.insert(0, [self.current_enum]) return keys def values(self): """Children entities.""" values = [ self.enum_entity ] for child_entiy in self.non_gui_children[self.current_enum].values(): values.append(child_entiy) return values def items(self): """Children entities paired with their key (key, value).""" items = [ (self.enum_key, self.enum_entity) ] for key, value in self.non_gui_children[self.current_enum].items(): items.append((key, value)) return items def set(self, value): """Set value.""" new_value = self.convert_to_valid_type(value) # First change value of enum key if available if self.enum_key in new_value: self.enum_entity.set(new_value.pop(self.enum_key)) for _key, _value in new_value.items(): self.non_gui_children[self.current_enum][_key].set(_value) def _item_initalization(self): self._default_metadata = NOT_SET self._studio_override_metadata = NOT_SET self._project_override_metadata = NOT_SET self._ignore_child_changes = False # `current_metadata` are still when schema is loaded # - only metadata stored with dict item are gorup overrides in # M_OVERRIDEN_KEY self._current_metadata = {} self._metadata_are_modified = False # Children are stored by key as keys are immutable and are defined by # schema self.valid_value_types = (dict, ) self.children = collections.defaultdict(list) self.non_gui_children = collections.defaultdict(dict) self.gui_layout = collections.defaultdict(list) if self.is_dynamic_item: self.require_key = False self.enum_key = self.schema_data.get("enum_key") self.enum_label = self.schema_data.get("enum_label") self.enum_children = self.schema_data.get("enum_children") self.enum_entity = None self.current_enum = None self._add_children() def schema_validations(self): """Validation of schema data.""" if self.enum_key is None: raise EntitySchemaError(self, "Key 'enum_key' is not set.") if not isinstance(self.enum_children, list): raise EntitySchemaError( self, "Key 'enum_children' must be a list. Got: {}".format( str(type(self.enum_children)) ) ) if not self.enum_children: raise EntitySchemaError(self, ( "Key 'enum_children' have empty value. Entity can't work" " without children definitions." )) children_def_keys = [] for children_def in self.enum_children: if not isinstance(children_def, dict): raise EntitySchemaError(( "Children definition under key 'enum_children' must" " be a dictionary." )) if "key" not in children_def: raise EntitySchemaError(( "Children definition under key 'enum_children' miss" " 'key' definition." )) # We don't validate regex of these keys because they will be stored # as value at the end. key = children_def["key"] if key in children_def_keys: # TODO this hould probably be different exception? raise SchemaDuplicatedKeys(self, key) children_def_keys.append(key) for children in self.children.values(): children_keys = set() children_keys.add(self.enum_key) for child_entity in children: if not isinstance(child_entity, BaseItemEntity): continue elif child_entity.key not in children_keys: children_keys.add(child_entity.key) else: raise SchemaDuplicatedKeys(self, child_entity.key) for children_by_key in self.non_gui_children.values(): for key in children_by_key.keys(): if not KEY_REGEX.match(key): raise InvalidKeySymbols(self.path, key) super(DictConditionalEntity, self).schema_validations() # Trigger schema validation on children entities for children in self.children.values(): for child_obj in children: child_obj.schema_validations() def on_change(self): """Update metadata on change and pass change to parent.""" self._update_current_metadata() for callback in self.on_change_callbacks: callback() self.parent.on_child_change(self) def on_child_change(self, child_obj): """Trigger on change callback if child changes are not ignored.""" if self._ignore_child_changes: return if ( child_obj is self.enum_entity or child_obj in self.children[self.current_enum] ): self.on_change() def _add_children(self): """Add children from schema data and repare enum items. Each enum item must have defined it's children. None are shared across all enum items. Nice to have: Have ability to have shared keys across all enum items. All children are stored by their enum item. """ # Skip and wait for validation if not self.enum_children or not self.enum_key: return enum_items = [] valid_enum_items = [] for item in self.enum_children: if isinstance(item, dict) and "key" in item: valid_enum_items.append(item) first_key = None for item in valid_enum_items: item_key = item["key"] if first_key is None: first_key = item_key item_label = item.get("label") or item_key enum_items.append({item_key: item_label}) if not enum_items: return self.current_enum = first_key enum_key = self.enum_key or "invalid" enum_schema = { "type": "enum", "multiselection": False, "enum_items": enum_items, "key": enum_key, "label": self.enum_label or enum_key } enum_entity = self.create_schema_object(enum_schema, self) self.enum_entity = enum_entity for item in valid_enum_items: item_key = item["key"] children = item.get("children") or [] for children_schema in children: child_obj = self.create_schema_object(children_schema, self) self.children[item_key].append(child_obj) self.gui_layout[item_key].append(child_obj) if isinstance(child_obj, GUIEntity): continue self.non_gui_children[item_key][child_obj.key] = child_obj def get_child_path(self, child_obj): """Get hierarchical path of child entity. Child must be entity's direct children. This must be possible to get for any children even if not from current enum value. """ if child_obj is self.enum_entity: return "/".join([self.path, self.enum_key]) result_key = None for children in self.non_gui_children.values(): for key, _child_obj in children.items(): if _child_obj is child_obj: result_key = key break if result_key is None: raise ValueError("Didn't found child {}".format(child_obj)) return "/".join([self.path, result_key]) def _update_current_metadata(self): current_metadata = {} for key, child_obj in self.non_gui_children[self.current_enum].items(): if self._override_state is OverrideState.DEFAULTS: break if not child_obj.is_group: continue if ( self._override_state is OverrideState.STUDIO and not child_obj.has_studio_override ): continue if ( self._override_state is OverrideState.PROJECT and not child_obj.has_project_override ): continue if M_OVERRIDEN_KEY not in current_metadata: current_metadata[M_OVERRIDEN_KEY] = [] current_metadata[M_OVERRIDEN_KEY].append(key) # Define if current metadata are avaialble for current override state metadata = NOT_SET if self._override_state is OverrideState.STUDIO: metadata = self._studio_override_metadata elif self._override_state is OverrideState.PROJECT: metadata = self._project_override_metadata if metadata is NOT_SET: metadata = {} self._metadata_are_modified = current_metadata != metadata self._current_metadata = current_metadata 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 # Change has/had override states self._override_state = state self.enum_entity.set_override_state(state) for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): child_obj.set_override_state(state) self._update_current_metadata() @property def value(self): output = { self.current_enum: self.enum_entity.value } for key, child_obj in self.non_gui_children[self.current_enum].items(): output[key] = child_obj.value return output @property def has_unsaved_changes(self): if self._metadata_are_modified: return True return self._child_has_unsaved_changes @property def _child_has_unsaved_changes(self): if self.enum_entity.has_unsaved_changes: return True for child_obj in self.non_gui_children[self.current_enum].values(): if child_obj.has_unsaved_changes: return True return False @property def has_studio_override(self): return self._child_has_studio_override @property def _child_has_studio_override(self): if self._override_state >= OverrideState.STUDIO: if self.enum_entity.has_studio_override: return True for child_obj in self.non_gui_children[self.current_enum].values(): if child_obj.has_studio_override: return True return False @property def has_project_override(self): return self._child_has_project_override @property def _child_has_project_override(self): if self._override_state >= OverrideState.PROJECT: if self.enum_entity.has_project_override: return True for child_obj in self.non_gui_children[self.current_enum].values(): if child_obj.has_project_override: return True return False def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET if self._override_state is OverrideState.DEFAULTS: output = { self.current_enum: self.enum_entity.settings_value() } non_gui_children = self.non_gui_children[self.current_enum] for key, child_obj in non_gui_children.items(): child_value = child_obj.settings_value() if not child_obj.is_file and not child_obj.file_item: for _key, _value in child_value.items(): new_key = "/".join([key, _key]) output[new_key] = _value else: output[key] = child_value return output 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 output = { self.current_enum: self.enum_entity.settings_value() } for key, child_obj in self.non_gui_children[self.current_enum].items(): value = child_obj.settings_value() if value is not NOT_SET: output[key] = value if not output: return NOT_SET output.update(self._current_metadata) return output def _prepare_value(self, value): if value is NOT_SET or self.enum_key not in value: return NOT_SET, NOT_SET enum_value = value.get(self.enum_key) if enum_value not in self.non_gui_children: return NOT_SET, NOT_SET # Create copy of value before poping values value = copy.deepcopy(value) metadata = {} for key in METADATA_KEYS: if key in value: metadata[key] = value.pop(key) enum_value = value.get(self.enum_key) old_metadata = metadata.get(M_OVERRIDEN_KEY) if old_metadata: old_metadata_set = set(old_metadata) new_metadata = [] non_gui_children = self.non_gui_children[enum_value] for key in non_gui_children.keys(): if key in old_metadata: new_metadata.append(key) old_metadata_set.remove(key) for key in old_metadata_set: new_metadata.append(key) metadata[M_OVERRIDEN_KEY] = new_metadata return value, metadata def update_default_value(self, value): """Update default values. Not an api method, should be called by parent. """ value = self._check_update_value(value, "default") self.has_default_value = value is not NOT_SET # TODO add value validation value, metadata = self._prepare_value(value) self._default_metadata = metadata if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key: child_obj.update_default_value(value) return value_keys = set(value.keys()) enum_value = value[self.enum_key] expected_keys = set(self.non_gui_children[enum_value]) expected_keys.add(self.enum_key) unknown_keys = value_keys - expected_keys if unknown_keys: self.log.warning( "{} Unknown keys in default values: {}".format( self.path, ", ".join("\"{}\"".format(key) for key in unknown_keys) ) ) self.enum_entity.update_default_value(enum_value) for children_by_key in self.non_gui_children.items(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_default_value(child_value) def update_studio_value(self, value): """Update studio override values. Not an api method, should be called by parent. """ value = self._check_update_value(value, "studio override") value, metadata = self._prepare_value(value) self._studio_override_metadata = metadata self.had_studio_override = metadata is not NOT_SET if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key: child_obj.update_default_value(value) return value_keys = set(value.keys()) enum_value = value[self.enum_key] expected_keys = set(self.non_gui_children[enum_value]) expected_keys.add(self.enum_key) unknown_keys = value_keys - expected_keys if unknown_keys: self.log.warning( "{} Unknown keys in studio overrides: {}".format( self.path, ", ".join("\"{}\"".format(key) for key in unknown_keys) ) ) self.enum_entity.update_studio_value(enum_value) for children_by_key in self.non_gui_children.items(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_studio_value(child_value) def update_project_value(self, value): """Update project override values. Not an api method, should be called by parent. """ value = self._check_update_value(value, "project override") value, metadata = self._prepare_value(value) self._project_override_metadata = metadata self.had_project_override = metadata is not NOT_SET if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key: child_obj.update_default_value(value) return value_keys = set(value.keys()) enum_value = value[self.enum_key] expected_keys = set(self.non_gui_children[enum_value]) expected_keys.add(self.enum_key) unknown_keys = value_keys - expected_keys if unknown_keys: self.log.warning( "{} Unknown keys in project overrides: {}".format( self.path, ", ".join("\"{}\"".format(key) for key in unknown_keys) ) ) self.enum_entity.update_project_value(enum_value) for children_by_key in self.non_gui_children.items(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_project_value(child_value)