diff --git a/pype/lib/anatomy.py b/pype/lib/anatomy.py index 67b8c01a56..4e7643dbbb 100644 --- a/pype/lib/anatomy.py +++ b/pype/lib/anatomy.py @@ -727,7 +727,7 @@ class Templates: key_2: "value_2" key_4: "value_3/value_2" """ - default_key_values = {} + default_key_values = templates.pop("defaults", {}) for key, value in tuple(templates.items()): if isinstance(value, dict): continue @@ -740,6 +740,19 @@ class Templates: key_values.update(sub_value) keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) + other_templates = templates.get("others") or {} + for sub_key, sub_value in other_templates.items(): + if sub_key in keys_by_subkey: + log.warning(( + "Key \"{}\" is duplicated in others. Skipping." + ).format(sub_key)) + continue + + key_values = {} + key_values.update(default_key_values) + key_values.update(sub_value) + keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) + default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values) for key, value in default_keys_by_subkeys.items(): diff --git a/pype/settings/defaults/project_anatomy/attributes.json b/pype/settings/defaults/project_anatomy/attributes.json index 8f35e41533..cc5516fd1f 100644 --- a/pype/settings/defaults/project_anatomy/attributes.json +++ b/pype/settings/defaults/project_anatomy/attributes.json @@ -10,20 +10,5 @@ "resolutionHeight": 1080, "pixelAspect": 1, "applications": [], - "task_short_names": { - "Generic": "gener", - "Art": "art", - "Modeling": "mdl", - "Texture": "tex", - "Lookdev": "look", - "Rigging": "rig", - "Edit": "edit", - "Layout": "lay", - "Setdress": "dress", - "Animation": "anim", - "FX": "fx", - "Lighting": "lgt", - "Paint": "paint", - "Compositing": "comp" - } + "tools": [] } \ No newline at end of file diff --git a/pype/settings/defaults/project_anatomy/tasks.json b/pype/settings/defaults/project_anatomy/tasks.json new file mode 100644 index 0000000000..74504cc4d7 --- /dev/null +++ b/pype/settings/defaults/project_anatomy/tasks.json @@ -0,0 +1,44 @@ +{ + "Generic": { + "short_name": "gener" + }, + "Art": { + "short_name": "art" + }, + "Modeling": { + "short_name": "mdl" + }, + "Texture": { + "short_name": "tex" + }, + "Lookdev": { + "short_name": "look" + }, + "Rigging": { + "short_name": "rig" + }, + "Edit": { + "short_name": "edit" + }, + "Layout": { + "short_name": "lay" + }, + "Setdress": { + "short_name": "dress" + }, + "Animation": { + "short_name": "anim" + }, + "FX": { + "short_name": "fx" + }, + "Lighting": { + "short_name": "lgt" + }, + "Paint": { + "short_name": "paint" + }, + "Compositing": { + "short_name": "comp" + } +} \ No newline at end of file diff --git a/pype/settings/defaults/project_anatomy/templates.json b/pype/settings/defaults/project_anatomy/templates.json index 397f7257fd..862b732846 100644 --- a/pype/settings/defaults/project_anatomy/templates.json +++ b/pype/settings/defaults/project_anatomy/templates.json @@ -1,8 +1,10 @@ { - "version_padding": 3, - "version": "v{version:0>{@version_padding}}", - "frame_padding": 4, - "frame": "{frame:0>{@frame_padding}}", + "defaults": { + "version_padding": 3, + "version": "v{version:0>{@version_padding}}", + "frame_padding": 4, + "frame": "{frame:0>{@frame_padding}}" + }, "work": { "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task}", "file": "{project[code]}_{asset}_{task}_{@version}<_{comment}>.{ext}", @@ -25,5 +27,5 @@ "path": "{@folder}/{@file}" }, "delivery": {}, - "other": {} + "others": {} } \ No newline at end of file diff --git a/pype/settings/entities/__init__.py b/pype/settings/entities/__init__.py index f67286832c..b48f763c73 100644 --- a/pype/settings/entities/__init__.py +++ b/pype/settings/entities/__init__.py @@ -90,12 +90,17 @@ from .input_entities import ( NumberEntity, BoolEntity, - EnumEntity, TextEntity, PathInput, RawJsonEntity ) +from .enum_entity import ( + EnumEntity, + AppsEnumEntity, + ToolsEnumEntity +) + from .list_entity import ListEntity from .dict_immutable_keys_entity import DictImmutableKeysEntity from .dict_mutable_keys_entity import DictMutableKeysEntity @@ -136,11 +141,14 @@ __all__ = ( "NumberEntity", "BoolEntity", - "EnumEntity", "TextEntity", "PathInput", "RawJsonEntity", + "EnumEntity", + "AppsEnumEntity", + "ToolsEnumEntity", + "ListEntity", "DictImmutableKeysEntity", diff --git a/pype/settings/entities/base_entity.py b/pype/settings/entities/base_entity.py index 9003a66d76..3a4bb23a90 100644 --- a/pype/settings/entities/base_entity.py +++ b/pype/settings/entities/base_entity.py @@ -203,6 +203,11 @@ class BaseItemEntity(BaseEntity): """Return path for a direct child entity.""" pass + @abstractmethod + def get_entity_from_path(self, path): + """Return system settings entity.""" + pass + def schema_validations(self): """Validate schema of entity and it's hierachy. @@ -790,6 +795,9 @@ class ItemEntity(BaseItemEntity): """Reference method for creation of entities defined in RootEntity.""" return self.root_item.create_schema_object(*args, **kwargs) + def get_entity_from_path(self, path): + return self.root_item.get_entity_from_path(path) + @abstractmethod def update_default_value(self, parent_values): """Fill default values on startup or on refresh. diff --git a/pype/settings/entities/enum_entity.py b/pype/settings/entities/enum_entity.py new file mode 100644 index 0000000000..4d6d268c70 --- /dev/null +++ b/pype/settings/entities/enum_entity.py @@ -0,0 +1,160 @@ +from .input_entities import InputEntity +from .lib import NOT_SET + + +class EnumEntity(InputEntity): + schema_types = ["enum"] + + def _item_initalization(self): + self.multiselection = self.schema_data.get("multiselection", False) + self.enum_items = self.schema_data["enum_items"] + if not self.enum_items: + raise ValueError("Attribute `enum_items` is not defined.") + + valid_keys = set() + for item in self.enum_items: + valid_keys.add(tuple(item.keys())[0]) + + self.valid_keys = valid_keys + + if self.multiselection: + self.valid_value_types = (list, ) + self.value_on_not_set = [] + else: + valid_value_types = set() + for key in valid_keys: + if self.value_on_not_set is NOT_SET: + self.value_on_not_set = key + valid_value_types.add(type(key)) + + self.valid_value_types = tuple(valid_value_types) + + # GUI attribute + self.placeholder = self.schema_data.get("placeholder") + + def schema_validations(self): + enum_keys = set() + for item in self.enum_items: + key = tuple(item.keys())[0] + if key in enum_keys: + raise ValueError( + "{}: Key \"{}\" is more than once in enum items.".format( + self.path, key + ) + ) + enum_keys.add(key) + + super(EnumEntity, self).schema_validations() + + def set(self, value): + if self.multiselection: + if not isinstance(value, list): + if isinstance(value, (set, tuple)): + value = list(value) + else: + value = [value] + check_values = value + else: + check_values = [value] + + self._validate_value_type(value) + + for item in check_values: + if item not in self.valid_keys: + raise ValueError( + "Invalid value \"{}\". Expected: {}".format( + item, self.valid_keys + ) + ) + self._current_value = value + self._on_value_change() + + +class AppsEnumEntity(EnumEntity): + schema_types = ["apps-enum"] + + def _item_initalization(self): + self.multiselection = True + self.value_on_not_set = [] + self.enum_items = [] + self.valid_keys = set() + self.valid_value_types = (list, ) + self.placeholder = None + + def _get_enum_values(self): + system_settings_entity = self.get_entity_from_path("system_settings") + + valid_keys = set() + enum_items = [] + for app_group in system_settings_entity["applications"].values(): + enabled_entity = app_group.get("enabled") + if enabled_entity and not enabled_entity.value: + continue + + host_name_entity = app_group.get("host_name") + if not host_name_entity or not host_name_entity.value: + continue + + group_label = app_group["label"].value + + for variant_name, variant_entity in app_group["variants"].items(): + enabled_entity = variant_entity.get("enabled") + if enabled_entity and not enabled_entity.value: + continue + + _group_label = variant_entity["label"].value + if not _group_label: + _group_label = group_label + variant_label = variant_entity["variant_label"].value + + full_label = "{} {}".format(_group_label, variant_label) + enum_items.append({variant_name: full_label}) + valid_keys.add(variant_name) + return enum_items, valid_keys + + def set_override_state(self, *args, **kwargs): + super(AppsEnumEntity, self).set_override_state(*args, **kwargs) + + self.enum_items, self.valid_keys = self._get_enum_values() + new_value = [] + for key in self._current_value: + if key in self.valid_keys: + new_value.append(key) + self._current_value = new_value + + +class ToolsEnumEntity(EnumEntity): + schema_types = ["tools-enum"] + + def _item_initalization(self): + self.multiselection = True + self.value_on_not_set = [] + self.enum_items = [] + self.valid_keys = set() + self.valid_value_types = (list, ) + self.placeholder = None + + def _get_enum_values(self): + system_settings_entity = self.get_entity_from_path("system_settings") + + valid_keys = set() + enum_items = [] + for tool_group in system_settings_entity["tools"].values(): + enabled_entity = tool_group.get("enabled") + if enabled_entity and not enabled_entity.value: + continue + + for variant_name in tool_group["variants"].keys(): + enum_items.append({variant_name: variant_name}) + valid_keys.add(variant_name) + return enum_items, valid_keys + + def set_override_state(self, *args, **kwargs): + super(ToolsEnumEntity, self).set_override_state(*args, **kwargs) + + self.enum_items, self.valid_keys = self._get_enum_values() + new_value = [] + for key in self._current_value: + if key in self.valid_keys: + new_value.append(key) + self._current_value = new_value diff --git a/pype/settings/entities/input_entities.py b/pype/settings/entities/input_entities.py index 0eaafb6c25..c26cb249a6 100644 --- a/pype/settings/entities/input_entities.py +++ b/pype/settings/entities/input_entities.py @@ -4,6 +4,7 @@ from abc import abstractmethod from .base_entity import ItemEntity from .lib import ( NOT_SET, + STRING_TYPE, OverrideState ) from .exceptions import ( @@ -349,79 +350,11 @@ class BoolEntity(InputEntity): self.value_on_not_set = True -class EnumEntity(InputEntity): - schema_types = ["enum"] - - def _item_initalization(self): - self.multiselection = self.schema_data.get("multiselection", False) - self.enum_items = self.schema_data["enum_items"] - if not self.enum_items: - raise ValueError("Attribute `enum_items` is not defined.") - - valid_keys = set() - for item in self.enum_items: - valid_keys.add(tuple(item.keys())[0]) - - self.valid_keys = valid_keys - - if self.multiselection: - self.valid_value_types = (list, ) - self.value_on_not_set = [] - else: - valid_value_types = set() - for key in valid_keys: - if self.value_on_not_set is NOT_SET: - self.value_on_not_set = key - valid_value_types.add(type(key)) - - self.valid_value_types = tuple(valid_value_types) - - # GUI attribute - self.placeholder = self.schema_data.get("placeholder") - - def schema_validations(self): - enum_keys = set() - for item in self.enum_items: - key = tuple(item.keys())[0] - if key in enum_keys: - raise ValueError( - "{}: Key \"{}\" is more than once in enum items.".format( - self.path, key - ) - ) - enum_keys.add(key) - - super(EnumEntity, self).schema_validations() - - def set(self, value): - if self.multiselection: - if not isinstance(value, list): - if isinstance(value, (set, tuple)): - value = list(value) - else: - value = [value] - check_values = value - else: - check_values = [value] - - self._validate_value_type(value) - - for item in check_values: - if item not in self.valid_keys: - raise ValueError( - "Invalid value \"{}\". Expected: {}".format( - item, self.valid_keys - ) - ) - self._current_value = value - self._on_value_change() - - class TextEntity(InputEntity): schema_types = ["text"] def _item_initalization(self): - self.valid_value_types = (str, ) + self.valid_value_types = (STRING_TYPE, ) self.value_on_not_set = "" # GUI attributes @@ -438,7 +371,7 @@ class PathInput(InputEntity): self.valid_value_types = (list, ) self.value_on_not_set = ["", ""] else: - self.valid_value_types = (str, ) + self.valid_value_types = (STRING_TYPE, ) self.value_on_not_set = "" diff --git a/pype/settings/entities/item_entities.py b/pype/settings/entities/item_entities.py index 2f2573721b..11e43e4fa6 100644 --- a/pype/settings/entities/item_entities.py +++ b/pype/settings/entities/item_entities.py @@ -1,5 +1,6 @@ from .lib import ( NOT_SET, + STRING_TYPE, OverrideState ) from .exceptions import ( @@ -56,7 +57,7 @@ class PathEntity(ItemEntity): # Create child object if not self.multiplatform and not self.multipath: - valid_value_types = (str, ) + valid_value_types = (STRING_TYPE, ) item_schema = { "type": "path-input", "key": self.key, diff --git a/pype/settings/entities/lib.py b/pype/settings/entities/lib.py index 1ca3f9efe0..ed3d7aed84 100644 --- a/pype/settings/entities/lib.py +++ b/pype/settings/entities/lib.py @@ -8,6 +8,10 @@ from .exceptions import ( SchemaDuplicatedEnvGroupKeys ) +try: + STRING_TYPE = basestring +except Exception: + STRING_TYPE = str WRAPPER_TYPES = ["form", "collapsible-wrap"] NOT_SET = type("NOT_SET", (), {"__bool__": lambda obj: False})() @@ -55,7 +59,7 @@ def _fill_schema_template_data( value, template_data, required_keys, missing_keys ) - elif isinstance(template, str): + elif isinstance(template, STRING_TYPE): # TODO find much better way how to handle filling template data for replacement_string in template_key_pattern.findall(template): key = str(replacement_string[1:-1]) @@ -233,7 +237,7 @@ def validate_schema(schema_data): validate_environment_groups_uniquenes(schema_data) -def gui_schema(subfolder, main_schema_name): +def get_gui_schema(subfolder, main_schema_name): dirpath = os.path.join( os.path.dirname(__file__), "schemas", @@ -269,6 +273,14 @@ def gui_schema(subfolder, main_schema_name): return main_schema +def get_studio_settings_schema(): + return get_gui_schema("system_schema", "schema_main") + + +def get_project_settings_schema(): + return get_gui_schema("projects_schema", "schema_main") + + class OverrideStateItem: """Object used as item for `OverrideState` enum. diff --git a/pype/settings/entities/root_entities.py b/pype/settings/entities/root_entities.py index 88b9da2428..b4dc667826 100644 --- a/pype/settings/entities/root_entities.py +++ b/pype/settings/entities/root_entities.py @@ -10,7 +10,8 @@ from .lib import ( NOT_SET, WRAPPER_TYPES, OverrideState, - gui_schema + get_studio_settings_schema, + get_project_settings_schema ) from pype.settings.constants import ( SYSTEM_SETTINGS_KEY, @@ -150,6 +151,12 @@ class RootEntity(BaseItemEntity): ).format(self.__class__.__name__)) child_entity.schema_validations() + def get_entity_from_path(self, path): + """Return system settings entity.""" + raise NotImplementedError(( + "Method `get_entity_from_path` not available for \"{}\"" + ).format(self.__class__.__name__)) + def create_schema_object(self, schema_data, *args, **kwargs): """Create entity by entered schema data. @@ -450,7 +457,7 @@ class SystemSettings(RootEntity): ): if schema_data is None: # Load system schemas - schema_data = gui_schema("system_schema", "schema_main") + schema_data = get_studio_settings_schema() super(SystemSettings, self).__init__(schema_data, reset) @@ -563,9 +570,11 @@ class ProjectSettings(RootEntity): ): self._project_name = project_name + self._system_settings_entity = None + if schema_data is None: # Load system schemas - schema_data = gui_schema("projects_schema", "schema_main") + schema_data = get_project_settings_schema() super(ProjectSettings, self).__init__(schema_data, reset) @@ -583,6 +592,40 @@ class ProjectSettings(RootEntity): def project_name(self, project_name): self.change_project(project_name) + @property + def system_settings_entity(self): + output = self._system_settings_entity + if output is None: + output = SystemSettings() + self._system_settings_entity = output + + if self.override_state is OverrideState.DEFAULTS: + if output.override_state is not OverrideState.DEFAULTS: + output.set_defaults_state() + elif self.override_state > OverrideState.DEFAULTS: + if output.override_state <= OverrideState.DEFAULTS: + try: + output.set_studio_state() + except Exception: + output.set_defaults_state() + return output + + def get_entity_from_path(self, path): + path_parts = path.split("/") + first_part = path_parts[0] + # TODO replace with constants + if first_part == "system_settings": + output = self.system_settings_entity + path_parts.pop(0) + else: + output = self + if first_part == "project_settings": + path_parts.pop(0) + + for path_part in path_parts: + output = output[path_part] + return output + def change_project(self, project_name): if project_name == self._project_name: return @@ -647,6 +690,8 @@ class ProjectSettings(RootEntity): if new_state is OverrideState.NOT_DEFINED: new_state = OverrideState.DEFAULTS + self._system_settings_entity = None + self._reset_values() self.set_override_state(new_state) diff --git a/pype/settings/entities/schemas/projects_schema/schema_main.json b/pype/settings/entities/schemas/projects_schema/schema_main.json index 828739e046..2ac6678d72 100644 --- a/pype/settings/entities/schemas/projects_schema/schema_main.json +++ b/pype/settings/entities/schemas/projects_schema/schema_main.json @@ -27,6 +27,23 @@ "type": "schema", "name": "schema_anatomy_attributes" }, + { + "type": "dict-modifiable", + "key": "tasks", + "label": "Task types", + "is_file": true, + "is_group": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "short_name", + "label": "Short name" + } + ] + } + }, { "type": "schema", "name": "schema_anatomy_imageio" diff --git a/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index 8925233bb8..f75319c7e1 100644 --- a/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -9,7 +9,9 @@ { "type": "number", "key": "fps", - "label": "Frame Rate" + "label": "Frame Rate", + "decimal": 2, + "minimum": 0 }, { "type": "number", @@ -54,27 +56,19 @@ { "type": "number", "key": "pixelAspect", - "label": "Pixel Aspect Ratio" + "label": "Pixel Aspect Ratio", + "decimal": 2, + "minimum": 0 }, { - "type": "enum", + "type": "apps-enum", "key": "applications", - "label": "Applications", - "multiselection": true, - "enum_items": [ - { "maya_2020": "Maya 2020" }, - { "nuke_12.2": "Nuke 12.2" }, - { "hiero_12.2": "Hiero 12.2" }, - { "houdini_18": "Houdini 18" }, - { "blender_2.91": "Blender 2.91" }, - { "aftereffects_2021": "After Effects 2021" } - ] + "label": "Applications" }, { - "type": "dict-modifiable", - "key": "task_short_names", - "label": "Task short names (by Task type)", - "object_type": "text" + "type": "tools-enum", + "key": "tools", + "label": "Tools" } ] } diff --git a/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json index 05718e0bc9..8410ec48f4 100644 --- a/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json +++ b/pype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_templates.json @@ -8,24 +8,37 @@ "is_group": true, "children": [ { - "type": "number", - "key": "version_padding", - "label": "Version Padding" + "type": "dict", + "key": "defaults", + "children": [ + { + "type": "number", + "key": "version_padding", + "label": "Version Padding", + "minimum": 1, + "maximum": 10 + }, + { + "type": "text", + "key": "version", + "label": "Version" + }, + { + "type": "number", + "key": "frame_padding", + "label": "Frame Padding", + "minimum": 1, + "maximum": 10 + }, + { + "type": "text", + "key": "frame", + "label": "Frame" + } + ] }, { - "type": "text", - "key": "version", - "label": "Version" - }, - { - "type": "number", - "key": "frame_padding", - "label": "Frame Padding" - }, - { - "type": "text", - "key": "frame", - "label": "Frame" + "type": "separator" }, { "type": "dict", @@ -128,9 +141,13 @@ }, { "type": "dict-modifiable", - "key": "other", - "label": "Other", - "object_type": "text" + "key": "others", + "label": "Others", + "collapsible_key": true, + "object_type": { + "type": "dict-modifiable", + "object_type": "text" + } } ] }