From 3e87997b401da0ba7e57ab0707b78ada9168ff2c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 10:54:05 +0200 Subject: [PATCH 001/115] modified how default settings are loaded --- openpype/settings/lib.py | 49 ++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 4a363910b8..04d8753869 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -329,6 +329,41 @@ def reset_default_settings(): _DEFAULT_SETTINGS = None +def _get_default_settings(): + from openpype.modules import get_module_settings_defs + + defaults = load_openpype_default_settings() + + module_settings_defs = get_module_settings_defs() + for module_settings_def_cls in module_settings_defs: + module_settings_def = module_settings_def_cls() + system_defaults = module_settings_def.get_system_defaults() + for path, value in system_defaults.items(): + if not path: + continue + + subdict = defaults["system_settings"] + path_items = list(path.split("/")) + last_key = path_items.pop(-1) + for key in path_items: + subdict = subdict[key] + subdict[last_key] = value + + project_defaults = module_settings_def.get_project_defaults() + for path, value in project_defaults.items(): + if not path: + continue + + subdict = defaults["project_settings"] + path_items = list(path.split("/")) + last_key = path_items.pop(-1) + for key in path_items: + subdict = subdict[key] + subdict[last_key] = value + + return defaults + + def get_default_settings(): """Get default settings. @@ -339,11 +374,11 @@ def get_default_settings(): dict: Loaded default settings. """ # TODO add cacher - return load_openpype_default_settings() - # global _DEFAULT_SETTINGS - # if _DEFAULT_SETTINGS is None: - # _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR) - # return copy.deepcopy(_DEFAULT_SETTINGS) + + global _DEFAULT_SETTINGS + if _DEFAULT_SETTINGS is None: + _DEFAULT_SETTINGS = _get_default_settings() + return copy.deepcopy(_DEFAULT_SETTINGS) def load_json_file(fpath): @@ -380,8 +415,8 @@ def load_jsons_from_dir(path, *args, **kwargs): "data1": "CONTENT OF FILE" }, "folder2": { - "data1": { - "subfolder1": "CONTENT OF FILE" + "subfolder1": { + "data2": "CONTENT OF FILE" } } } From aa2f5d85701fa56ed8eb5b1a568dceced3c2ead1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 10:54:31 +0200 Subject: [PATCH 002/115] defined class which defined base settings --- openpype/modules/__init__.py | 13 ++++++- openpype/modules/base.py | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 81853faa38..261d65d2ee 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -1,16 +1,25 @@ # -*- coding: utf-8 -*- from .base import ( OpenPypeModule, + OpenPypeAddOn, OpenPypeInterface, + ModulesManager, - TrayModulesManager + TrayModulesManager, + + ModuleSettingsDef, + get_module_settings_defs ) __all__ = ( "OpenPypeModule", + "OpenPypeAddOn", "OpenPypeInterface", "ModulesManager", - "TrayModulesManager" + "TrayModulesManager", + + "ModuleSettingsDef", + "get_module_settings_defs" ) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index d43d5635d1..18bbb75cec 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -920,3 +920,74 @@ class TrayModulesManager(ModulesManager): ), exc_info=True ) + + +def get_module_settings_defs(): + load_modules() + + import openpype_modules + + settings_defs = [] + + log = PypeLogger.get_logger("ModuleSettingsLoad") + + for raw_module in openpype_modules: + for attr_name in dir(raw_module): + attr = getattr(raw_module, attr_name) + if ( + not inspect.isclass(attr) + or attr is ModuleSettingsDef + or not issubclass(attr, ModuleSettingsDef) + ): + continue + + if inspect.isabstract(attr): + # Find missing implementations by convetion on `abc` module + not_implemented = [] + for attr_name in dir(attr): + attr = getattr(attr, attr_name, None) + abs_method = getattr( + attr, "__isabstractmethod__", None + ) + if attr and abs_method: + not_implemented.append(attr_name) + + # Log missing implementations + log.warning(( + "Skipping abstract Class: {} in module {}." + " Missing implementations: {}" + ).format( + attr_name, raw_module.__name__, ", ".join(not_implemented) + )) + continue + + settings_defs.append(attr) + + return settings_defs + + +@six.add_metaclass(ABCMeta) +class ModuleSettingsDef: + @abstractmethod + def get_system_schemas(self): + pass + + @abstractmethod + def get_project_schemas(self): + pass + + @abstractmethod + def save_system_defaults(self, data): + pass + + @abstractmethod + def save_project_defaults(self, data): + pass + + @abstractmethod + def get_system_defaults(self): + pass + + @abstractmethod + def get_project_defaults(self): + pass From f76b5b08679f1e327d1047b5e5217a3e662dc37f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 14:28:12 +0200 Subject: [PATCH 003/115] use constants for schema keys --- openpype/settings/entities/lib.py | 3 +++ openpype/settings/entities/root_entities.py | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index e58281644a..307792edc9 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -23,6 +23,9 @@ TEMPLATE_METADATA_KEYS = ( DEFAULT_VALUES_KEY, ) +SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" +SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" + template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 00677480e8..39b5cb5096 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -9,6 +9,8 @@ from .base_entity import BaseItemEntity from .lib import ( NOT_SET, WRAPPER_TYPES, + SCHEMA_KEY_SYSTEM_SETTINGS, + SCHEMA_KEY_PROJECT_SETTINGS, OverrideState, SchemasHub ) @@ -468,7 +470,7 @@ class SystemSettings(RootEntity): ): if schema_hub is None: # Load system schemas - schema_hub = SchemasHub("system_schema") + schema_hub = SchemasHub(SCHEMA_KEY_SYSTEM_SETTINGS) super(SystemSettings, self).__init__(schema_hub, reset) @@ -599,7 +601,7 @@ class ProjectSettings(RootEntity): if schema_hub is None: # Load system schemas - schema_hub = SchemasHub("projects_schema") + schema_hub = SchemasHub(SCHEMA_KEY_PROJECT_SETTINGS) super(ProjectSettings, self).__init__(schema_hub, reset) From 50e2fce229992d8f1ca5800b3dcaff3796604cd4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 14:28:19 +0200 Subject: [PATCH 004/115] remove TODO --- openpype/settings/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 04d8753869..04e8bffd8f 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -373,8 +373,6 @@ def get_default_settings(): Returns: dict: Loaded default settings. """ - # TODO add cacher - global _DEFAULT_SETTINGS if _DEFAULT_SETTINGS is None: _DEFAULT_SETTINGS = _get_default_settings() From 5db15b273e2a6a36309c51067f432d7d784b369e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 18:20:08 +0200 Subject: [PATCH 005/115] settings def has id --- openpype/modules/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 18bbb75cec..8b575bc8cd 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -968,6 +968,14 @@ def get_module_settings_defs(): @six.add_metaclass(ABCMeta) class ModuleSettingsDef: + _id = None + + @property + def id(self): + if self._id is None: + self._id = uuid4() + return self._id + @abstractmethod def get_system_schemas(self): pass From 9d7f0db6d8177860930e533d082bf7e549f2e074 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 18:25:07 +0200 Subject: [PATCH 006/115] changed how schemas are get from openpype --- openpype/modules/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 8b575bc8cd..3d3d7ae6cb 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -977,11 +977,11 @@ class ModuleSettingsDef: return self._id @abstractmethod - def get_system_schemas(self): + def get_settings_schemas(self, schema_type): pass @abstractmethod - def get_project_schemas(self): + def get_dynamic_schemas(self, schema_type): pass @abstractmethod From 5099f5f853d96e5846c7eab31aa0557c3c2ff4da Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:33:28 +0200 Subject: [PATCH 007/115] small condition modifications --- openpype/settings/entities/base_entity.py | 2 +- openpype/settings/entities/dict_conditional.py | 2 +- openpype/settings/entities/dict_mutable_keys_entity.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index b4ebe885f5..d9dcf633e5 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -253,7 +253,7 @@ class BaseItemEntity(BaseEntity): # Validate that env group entities will be stored into file. # - env group entities must store metadata which is not possible if # metadata would be outside of file - if not self.file_item and self.is_env_group: + if self.file_item is None and self.is_env_group: reason = ( "Environment item is not inside file" " item so can't store metadata for defaults." diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d275d8ac3d..fc7cbfdee5 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -469,7 +469,7 @@ class DictConditionalEntity(ItemEntity): output = {} for key, child_obj in children_items: child_value = child_obj.settings_value() - if not child_obj.is_file and not child_obj.file_item: + if not child_obj.is_file and child_obj.file_item is None: for _key, _value in child_value.items(): new_key = "/".join([key, _key]) output[new_key] = _value diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index c3df935269..f75fb23d82 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -261,7 +261,7 @@ class DictMutableKeysEntity(EndpointEntity): raise EntitySchemaError(self, reason) # TODO Ability to store labels should be defined with different key - if self.collapsible_key and not self.file_item: + if self.collapsible_key and self.file_item is None: reason = ( "Modifiable dictionary with collapsible keys is not under" " file item so can't store metadata." From 13720249575860a6aa01c1b7b2e1f21d3ccfefc6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:34:26 +0200 Subject: [PATCH 008/115] added loading of setttings modules definitions in SchemaHub --- openpype/settings/entities/lib.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 307792edc9..a72908967f 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -117,14 +117,27 @@ class SchemasHub: # It doesn't make sence to reload types on each reset as they can't be # changed self._load_types() + # Attributes for modules settings + self._modules_settings_defs_by_id = {} + self._dynamic_schemas_by_module_id = {} # Trigger reset if reset: self.reset() def reset(self): + self._load_modules_settings_defs() self._load_schemas() + def _load_modules_settings_defs(self): + from openpype.modules import get_module_settings_defs + + module_settings_defs = get_module_settings_defs() + for module_settings_def_cls in module_settings_defs: + module_settings_def = module_settings_def_cls() + def_id = module_settings_def.id + self._modules_settings_defs_by_id[def_id] = module_settings_def + @property def gui_types(self): return self._gui_types From 2e9e0ba09ff17393b90d18717b0f4fc94e09cb27 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:34:52 +0200 Subject: [PATCH 009/115] use constant for extending schema types --- openpype/settings/entities/lib.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index a72908967f..4b6ed5a365 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -26,6 +26,10 @@ TEMPLATE_METADATA_KEYS = ( SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" +SCHEMA_EXTEND_TYPES = ( + "schema", "template", "schema_template", "dynamic_schema" +) + template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") @@ -217,7 +221,7 @@ class SchemasHub: list: Resolved schema data. """ schema_type = schema_data["type"] - if schema_type not in ("schema", "template", "schema_template"): + if schema_type not in SCHEMA_EXTEND_TYPES: return [schema_data] if schema_type == "schema": From 2bb74c68e9b8ba41de9268f95c0264f4201bc98a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:35:48 +0200 Subject: [PATCH 010/115] added resolving of dynamic module items --- openpype/settings/entities/lib.py | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 4b6ed5a365..dd216f4d90 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -146,6 +146,23 @@ class SchemasHub: def gui_types(self): return self._gui_types + def resolve_dynamic_schema(self, dynamic_key): + output = [] + for def_id, def_keys in self._dynamic_schemas_by_module_id.items(): + if dynamic_key in def_keys: + def_schema = def_keys[dynamic_key] + if not def_schema: + continue + + if isinstance(def_schema, dict): + def_schema = [def_schema] + + for item in def_schema: + item["_module_id"] = def_id + item["_module_store_key"] = dynamic_key + output.extend(def_schema) + return output + def get_schema(self, schema_name): """Get schema definition data by it's name. @@ -229,6 +246,9 @@ class SchemasHub: self.get_schema(schema_data["name"]) ) + if schema_type == "dynamic_schema": + return self.resolve_dynamic_schema(schema_data["name"]) + template_name = schema_data["name"] template_def = self.get_template(template_name) @@ -329,6 +349,7 @@ class SchemasHub: self._crashed_on_load = {} self._loaded_templates = {} self._loaded_schemas = {} + self._dynamic_schemas_by_module_id = {} dirpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -337,6 +358,7 @@ class SchemasHub: ) loaded_schemas = {} loaded_templates = {} + dynamic_schemas_by_module_id = {} for root, _, filenames in os.walk(dirpath): for filename in filenames: basename, ext = os.path.splitext(filename) @@ -386,8 +408,31 @@ class SchemasHub: ) loaded_schemas[basename] = schema_data + defs_iter = self._modules_settings_defs_by_id.items() + for def_id, module_settings_def in defs_iter: + dynamic_schemas_by_module_id[def_id] = ( + module_settings_def.get_dynamic_schemas(self._schema_subfolder) + ) + module_schemas = module_settings_def.get_settings_schemas( + self._schema_subfolder + ) + for key, schema_data in module_schemas.items(): + if isinstance(schema_data, list): + if key in loaded_templates: + raise KeyError( + "Duplicated template key \"{}\"".format(key) + ) + loaded_templates[key] = schema_data + else: + if key in loaded_schemas: + raise KeyError( + "Duplicated schema key \"{}\"".format(key) + ) + loaded_schemas[key] = schema_data + self._loaded_templates = loaded_templates self._loaded_schemas = loaded_schemas + self._dynamic_schemas_by_module_id = dynamic_schemas_by_module_id def _fill_template(self, child_data, template_def): """Fill template based on schema definition and template definition. From b648dd7dc325029906cf69dd8a6887ee31e567ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 10:35:58 +0200 Subject: [PATCH 011/115] load types on each reset --- openpype/settings/entities/lib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index dd216f4d90..91e66eec8e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -118,9 +118,6 @@ class SchemasHub: self._loaded_templates = {} self._loaded_schemas = {} - # It doesn't make sence to reload types on each reset as they can't be - # changed - self._load_types() # Attributes for modules settings self._modules_settings_defs_by_id = {} self._dynamic_schemas_by_module_id = {} @@ -131,6 +128,7 @@ class SchemasHub: def reset(self): self._load_modules_settings_defs() + self._load_types() self._load_schemas() def _load_modules_settings_defs(self): From 32011838b3e4404c09249b2b60c69e533d63b300 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:10:33 +0200 Subject: [PATCH 012/115] renamed variable --- openpype/settings/entities/lib.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 91e66eec8e..b87845b95e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -120,7 +120,7 @@ class SchemasHub: # Attributes for modules settings self._modules_settings_defs_by_id = {} - self._dynamic_schemas_by_module_id = {} + self._dynamic_schemas_def_by_id = {} # Trigger reset if reset: @@ -146,7 +146,7 @@ class SchemasHub: def resolve_dynamic_schema(self, dynamic_key): output = [] - for def_id, def_keys in self._dynamic_schemas_by_module_id.items(): + for def_id, def_keys in self._dynamic_schemas_def_by_id.items(): if dynamic_key in def_keys: def_schema = def_keys[dynamic_key] if not def_schema: @@ -347,7 +347,7 @@ class SchemasHub: self._crashed_on_load = {} self._loaded_templates = {} self._loaded_schemas = {} - self._dynamic_schemas_by_module_id = {} + self._dynamic_schemas_def_by_id = {} dirpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), @@ -356,7 +356,7 @@ class SchemasHub: ) loaded_schemas = {} loaded_templates = {} - dynamic_schemas_by_module_id = {} + dynamic_schemas_def_by_id = {} for root, _, filenames in os.walk(dirpath): for filename in filenames: basename, ext = os.path.splitext(filename) @@ -408,7 +408,7 @@ class SchemasHub: defs_iter = self._modules_settings_defs_by_id.items() for def_id, module_settings_def in defs_iter: - dynamic_schemas_by_module_id[def_id] = ( + dynamic_schemas_def_by_id[def_id] = ( module_settings_def.get_dynamic_schemas(self._schema_subfolder) ) module_schemas = module_settings_def.get_settings_schemas( @@ -430,7 +430,7 @@ class SchemasHub: self._loaded_templates = loaded_templates self._loaded_schemas = loaded_schemas - self._dynamic_schemas_by_module_id = dynamic_schemas_by_module_id + self._dynamic_schemas_def_by_id = dynamic_schemas_def_by_id def _fill_template(self, child_data, template_def): """Fill template based on schema definition and template definition. From f38c7a462e96755d6dce8255becef57810de9b06 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:15:21 +0200 Subject: [PATCH 013/115] added few attributes for dynamic schemas --- openpype/settings/entities/base_entity.py | 12 ++++++++++++ openpype/settings/entities/lib.py | 3 +-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index d9dcf633e5..832c8ab854 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -104,6 +104,12 @@ class BaseItemEntity(BaseEntity): self.is_group = False # Entity's value will be stored into file with name of it's key self.is_file = False + # Default values are not stored to an openpype file + # - these must not be set through schemas directly + self.dynamic_schema_id = None + self.is_dynamic_schema_node = False + self.is_in_dynamic_schema_node = False + # Reference to parent entity which has `is_group` == True # - stays as None if none of parents is group self.group_item = None @@ -800,6 +806,12 @@ class ItemEntity(BaseItemEntity): self.is_dynamic_item = is_dynamic_item self.is_file = self.schema_data.get("is_file", False) + # These keys have underscore as they must not be set in schemas + self.dynamic_schema_id = self.schema_data.get( + "_dynamic_schema_id", None + ) + self.is_dynamic_schema_node = self.dynamic_schema_id is not None + self.is_group = self.schema_data.get("is_group", False) self.is_in_dynamic_item = bool( not self.is_dynamic_item diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index b87845b95e..2a1bbaa115 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -156,8 +156,7 @@ class SchemasHub: def_schema = [def_schema] for item in def_schema: - item["_module_id"] = def_id - item["_module_store_key"] = dynamic_key + item["_dynamic_schema_id"] = def_id output.extend(def_schema) return output From 5541b3fd0cc8fd5fe60cb11a8c22dfbc8f4911db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:16:57 +0200 Subject: [PATCH 014/115] added few conditions so it is possbile to load dynamic schemas --- openpype/settings/entities/base_entity.py | 25 ++++++++++++++++---- openpype/settings/entities/input_entities.py | 6 ++++- openpype/settings/entities/item_entities.py | 7 +++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 832c8ab854..bea90882a7 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -841,10 +841,20 @@ class ItemEntity(BaseItemEntity): self._require_restart_on_change = require_restart_on_change # File item reference - if self.parent.is_file: - self.file_item = self.parent - elif self.parent.file_item: - self.file_item = self.parent.file_item + if not self.is_dynamic_schema_node: + self.is_in_dynamic_schema_node = ( + self.parent.is_dynamic_schema_node + or self.parent.is_in_dynamic_schema_node + ) + + if ( + not self.is_dynamic_schema_node + and not self.is_in_dynamic_schema_node + ): + if self.parent.is_file: + self.file_item = self.parent + elif self.parent.file_item: + self.file_item = self.parent.file_item # Group item reference if self.parent.is_group: @@ -903,7 +913,12 @@ class ItemEntity(BaseItemEntity): ) raise EntitySchemaError(self, reason) - if self.is_file and self.file_item is not None: + if ( + not self.is_dynamic_schema_node + and not self.is_in_dynamic_schema_node + and self.is_file + and self.file_item is not None + ): reason = ( "Entity has set `is_file` to true but" " it's parent is already marked as file item." diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 6952529963..b65c1c440e 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -116,7 +116,11 @@ class InputEntity(EndpointEntity): def schema_validations(self): # Input entity must have file parent. - if not self.file_item: + if ( + not self.is_dynamic_schema_node + and not self.is_in_dynamic_schema_node + and self.file_item is None + ): raise EntitySchemaError(self, "Missing parent file entity.") super(InputEntity, self).schema_validations() diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 7e84f8c801..1e4f1025cc 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -215,7 +215,12 @@ class ListStrictEntity(ItemEntity): def schema_validations(self): # List entity must have file parent. - if not self.file_item and not self.is_file: + if ( + not self.is_dynamic_schema_node + and not self.is_in_dynamic_schema_node + and not self.is_file + and self.file_item is None + ): raise EntitySchemaError( self, "Missing file entity in hierarchy." ) From eaa1499510566f5d6ebc8308db50b4c5d8780c5a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:17:17 +0200 Subject: [PATCH 015/115] conditional dict does not care about paths as it must be group --- openpype/settings/entities/dict_conditional.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index fc7cbfdee5..8a944e5fdc 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -468,13 +468,7 @@ class DictConditionalEntity(ItemEntity): output = {} for key, child_obj in children_items: - child_value = child_obj.settings_value() - if not child_obj.is_file and child_obj.file_item is None: - for _key, _value in child_value.items(): - new_key = "/".join([key, _key]) - output[new_key] = _value - else: - output[key] = child_value + output[key] = child_obj.settings_value() return output if self.is_group: From f65dee0a0e8b40f42eb8485faf1377d01542f52a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:22:53 +0200 Subject: [PATCH 016/115] added method which collects dynamic schema entities --- openpype/settings/entities/base_entity.py | 22 ++++++++++++++++++- .../settings/entities/dict_conditional.py | 4 ++++ .../entities/dict_immutable_keys_entity.py | 7 ++++++ openpype/settings/entities/input_entities.py | 4 ++++ openpype/settings/entities/item_entities.py | 7 ++++++ openpype/settings/entities/lib.py | 9 ++++++++ openpype/settings/entities/root_entities.py | 13 ++++++++++- 7 files changed, 64 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index bea90882a7..0d2923f9e0 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -476,7 +476,15 @@ class BaseItemEntity(BaseEntity): @abstractmethod def settings_value(self): - """Value of an item without key.""" + """Value of an item without key without dynamic items.""" + pass + + @abstractmethod + def collect_dynamic_schema_entities(self): + """Collect entities that are on top of dynamically added schemas. + + This method make sence only when defaults are saved. + """ pass @abstractmethod @@ -905,6 +913,18 @@ class ItemEntity(BaseItemEntity): def root_key(self): return self.root_item.root_key + @abstractmethod + def collect_dynamic_schema_entities(self, collector): + """Collect entities that are on top of dynamically added schemas. + + This method make sence only when defaults are saved. + + Args: + collector(DynamicSchemaValueCollector): Object where dynamic + entities are stored. + """ + pass + def schema_validations(self): if not self.label and self.use_label_wrap: reason = ( diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8a944e5fdc..44775e9113 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -455,6 +455,10 @@ class DictConditionalEntity(ItemEntity): return True return False + def collect_dynamic_schema_entities(self, collector): + if self.is_dynamic_schema_node: + collector.add_entity(self) + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index bde5304787..24cd9401b9 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -318,6 +318,13 @@ class DictImmutableKeysEntity(ItemEntity): return True return False + def collect_dynamic_schema_entities(self, collector): + for child_obj in self.non_gui_children.values(): + child_obj.collect_dynamic_schema_entities(collector) + + if self.is_dynamic_schema_node: + collector.add_entity(self) + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index b65c1c440e..469fdee310 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -49,6 +49,10 @@ class EndpointEntity(ItemEntity): super(EndpointEntity, self).schema_validations() + def collect_dynamic_schema_entities(self, collector): + if self.is_dynamic_schema_node: + collector.add_entity(self) + @abstractmethod def _settings_value(self): pass diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 1e4f1025cc..3823a25c60 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -112,6 +112,9 @@ class PathEntity(ItemEntity): def set(self, value): self.child_obj.set(value) + def collect_dynamic_schema_entities(self, *args, **kwargs): + self.child_obj.collect_dynamic_schema_entities(*args, **kwargs) + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET @@ -251,6 +254,10 @@ class ListStrictEntity(ItemEntity): for idx, item in enumerate(new_value): self.children[idx].set(item) + def collect_dynamic_schema_entities(self, collector): + if self.is_dynamic_schema_node: + collector.add_entity(self) + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 2a1bbaa115..98dede39e8 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -663,3 +663,12 @@ class SchemasHub: if found_idx is not None: metadata_item = template_def.pop(found_idx) return metadata_item + + +class DynamicSchemaValueCollector: + def __init__(self, schema_hub): + self._schema_hub = schema_hub + self._dynamic_entities = [] + + def add_entity(self, entity): + self._dynamic_entities.append(entity) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 39b5cb5096..2c88016344 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -12,7 +12,8 @@ from .lib import ( SCHEMA_KEY_SYSTEM_SETTINGS, SCHEMA_KEY_PROJECT_SETTINGS, OverrideState, - SchemasHub + SchemasHub, + DynamicSchemaValueCollector ) from .exceptions import ( SchemaError, @@ -259,6 +260,16 @@ class RootEntity(BaseItemEntity): output[key] = child_obj.value return output + def collect_dynamic_schema_entities(self): + output = DynamicSchemaValueCollector(self.schema_hub) + if self._override_state is not OverrideState.DEFAULTS: + return output + + for child_obj in self.non_gui_children.values(): + child_obj.collect_dynamic_schema_entities(output) + + return output + def settings_value(self): """Value for current override state with metadata. From 6282e8d1ad57e85e7c8c8e11ddb201b4bf56b21a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:23:23 +0200 Subject: [PATCH 017/115] skip dynamic schema entities in settings values method --- openpype/settings/entities/dict_immutable_keys_entity.py | 3 +++ openpype/settings/entities/root_entities.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 24cd9401b9..a81a64c183 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -332,6 +332,9 @@ class DictImmutableKeysEntity(ItemEntity): if self._override_state is OverrideState.DEFAULTS: output = {} for key, child_obj in self.non_gui_children.items(): + if child_obj.is_dynamic_schema_node: + continue + 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(): diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 2c88016344..b178e3fa36 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -281,6 +281,8 @@ class RootEntity(BaseItemEntity): if self._override_state is not OverrideState.DEFAULTS: output = {} for key, child_obj in self.non_gui_children.items(): + if child_obj.is_dynamic_schema_node: + continue value = child_obj.settings_value() if value is not NOT_SET: output[key] = value From 37ef6d022355f7f325b933c6192665c5811cde6c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:23:39 +0200 Subject: [PATCH 018/115] ignore file handling for dynamic schema nodes --- openpype/settings/entities/dict_immutable_keys_entity.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index a81a64c183..8871a3a3d9 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -330,13 +330,20 @@ class DictImmutableKeysEntity(ItemEntity): return NOT_SET if self._override_state is OverrideState.DEFAULTS: + is_dynamic_schema_node = ( + self.is_dynamic_schema_node or self.is_in_dynamic_schema_node + ) output = {} for key, child_obj in self.non_gui_children.items(): if child_obj.is_dynamic_schema_node: continue child_value = child_obj.settings_value() - if not child_obj.is_file and not child_obj.file_item: + if ( + not is_dynamic_schema_node + and 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 From cf9114b0f1dfbea06bb8b688dbfa3627a885d41c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:23:57 +0200 Subject: [PATCH 019/115] added schema validation of dynamic schemas --- openpype/settings/entities/base_entity.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 0d2923f9e0..f5f5b4d761 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -253,9 +253,18 @@ class BaseItemEntity(BaseEntity): ) # Group item can be only once in on hierarchy branch. - if self.is_group and self.group_item: + if self.is_group and self.group_item is not None: raise SchemeGroupHierarchyBug(self) + # Group item can be only once in on hierarchy branch. + if self.group_item is not None and self.is_dynamic_schema_node: + reason = ( + "Dynamic schema is inside grouped item {}." + " Change group hierarchy or remove dynamic" + " schema to be able work properly." + ).format(self.group_item.path) + raise EntitySchemaError(self, reason) + # Validate that env group entities will be stored into file. # - env group entities must store metadata which is not possible if # metadata would be outside of file From fff590f7f88aaf0130adfc7cf6acd98cc6dac05c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 12:43:16 +0200 Subject: [PATCH 020/115] add getter method for dynamic schema definitions --- openpype/settings/entities/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 98dede39e8..3877b49648 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -431,6 +431,9 @@ class SchemasHub: self._loaded_schemas = loaded_schemas self._dynamic_schemas_def_by_id = dynamic_schemas_def_by_id + def get_dynamic_schema_def(self, schema_def_id): + return self._dynamic_schemas_def_by_id.get(schema_def_id) + def _fill_template(self, child_data, template_def): """Fill template based on schema definition and template definition. From b507b4e9d33b6a65003954545f7fa271030d995d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:02:08 +0200 Subject: [PATCH 021/115] modified dynamic schemas attributes --- openpype/settings/entities/lib.py | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 3877b49648..457468b18b 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -108,8 +108,8 @@ class OverrideState: class SchemasHub: - def __init__(self, schema_subfolder, reset=True): - self._schema_subfolder = schema_subfolder + def __init__(self, schema_type, reset=True): + self._schema_type = schema_type self._loaded_types = {} self._gui_types = tuple() @@ -119,13 +119,17 @@ class SchemasHub: self._loaded_schemas = {} # Attributes for modules settings - self._modules_settings_defs_by_id = {} - self._dynamic_schemas_def_by_id = {} + self._dynamic_schemas_defs_by_id = {} + self._dynamic_schemas_by_id = {} # Trigger reset if reset: self.reset() + @property + def schema_type(self): + return self._schema_type + def reset(self): self._load_modules_settings_defs() self._load_types() @@ -138,7 +142,7 @@ class SchemasHub: for module_settings_def_cls in module_settings_defs: module_settings_def = module_settings_def_cls() def_id = module_settings_def.id - self._modules_settings_defs_by_id[def_id] = module_settings_def + self._dynamic_schemas_defs_by_id[def_id] = module_settings_def @property def gui_types(self): @@ -146,7 +150,7 @@ class SchemasHub: def resolve_dynamic_schema(self, dynamic_key): output = [] - for def_id, def_keys in self._dynamic_schemas_def_by_id.items(): + for def_id, def_keys in self._dynamic_schemas_by_id.items(): if dynamic_key in def_keys: def_schema = def_keys[dynamic_key] if not def_schema: @@ -346,16 +350,16 @@ class SchemasHub: self._crashed_on_load = {} self._loaded_templates = {} self._loaded_schemas = {} - self._dynamic_schemas_def_by_id = {} + self._dynamic_schemas_by_id = {} dirpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), "schemas", - self._schema_subfolder + self.schema_type ) loaded_schemas = {} loaded_templates = {} - dynamic_schemas_def_by_id = {} + dynamic_schemas_by_id = {} for root, _, filenames in os.walk(dirpath): for filename in filenames: basename, ext = os.path.splitext(filename) @@ -405,13 +409,13 @@ class SchemasHub: ) loaded_schemas[basename] = schema_data - defs_iter = self._modules_settings_defs_by_id.items() + defs_iter = self._dynamic_schemas_defs_by_id.items() for def_id, module_settings_def in defs_iter: - dynamic_schemas_def_by_id[def_id] = ( - module_settings_def.get_dynamic_schemas(self._schema_subfolder) + dynamic_schemas_by_id[def_id] = ( + module_settings_def.get_dynamic_schemas(self.schema_type) ) module_schemas = module_settings_def.get_settings_schemas( - self._schema_subfolder + self.schema_type ) for key, schema_data in module_schemas.items(): if isinstance(schema_data, list): @@ -429,10 +433,10 @@ class SchemasHub: self._loaded_templates = loaded_templates self._loaded_schemas = loaded_schemas - self._dynamic_schemas_def_by_id = dynamic_schemas_def_by_id + self._dynamic_schemas_by_id = dynamic_schemas_by_id - def get_dynamic_schema_def(self, schema_def_id): - return self._dynamic_schemas_def_by_id.get(schema_def_id) + def get_dynamic_modules_settings_defs(self, schema_def_id): + return self._dynamic_schemas_defs_by_id.get(schema_def_id) def _fill_template(self, child_data, template_def): """Fill template based on schema definition and template definition. From 90076d519f389a8a5a50e1df5f9771294a2128b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:02:45 +0200 Subject: [PATCH 022/115] removed project_settings getter --- openpype/settings/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 04e8bffd8f..d7684082f3 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -354,7 +354,7 @@ def _get_default_settings(): if not path: continue - subdict = defaults["project_settings"] + subdict = defaults path_items = list(path.split("/")) last_key = path_items.pop(-1) for key in path_items: From 9d31ec70116589ab0d00bf6a6c0d840420999924 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:03:43 +0200 Subject: [PATCH 023/115] implemented save for dynamic schemas --- openpype/settings/entities/lib.py | 21 +++++++++++++++++++++ openpype/settings/entities/root_entities.py | 3 +++ 2 files changed, 24 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 457468b18b..13037ac373 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -3,6 +3,7 @@ import re import json import copy import inspect +import collections from .exceptions import ( SchemaTemplateMissingKeys, @@ -679,3 +680,23 @@ class DynamicSchemaValueCollector: def add_entity(self, entity): self._dynamic_entities.append(entity) + + def create_hierarchy(self): + output = collections.defaultdict(dict) + for entity in self._dynamic_entities: + output[entity.dynamic_schema_id][entity.path] = ( + entity.settings_value() + ) + return output + + def save_values(self): + hierarchy = self.create_hierarchy() + + for schema_def_id, schema_def_value in hierarchy.items(): + schema_def = self._schema_hub.get_dynamic_modules_settings_defs( + schema_def_id + ) + if self._schema_hub.schema_type == SCHEMA_KEY_SYSTEM_SETTINGS: + schema_def.save_system_defaults(schema_def_value) + elif self._schema_hub.schema_type == SCHEMA_KEY_PROJECT_SETTINGS: + schema_def.save_project_defaults(schema_def_value) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index b178e3fa36..6f444d5394 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -428,6 +428,9 @@ class RootEntity(BaseItemEntity): with open(output_path, "w") as file_stream: json.dump(value, file_stream, indent=4) + dynamic_values_item = self.collect_dynamic_schema_entities() + dynamic_values_item.save_values() + @abstractmethod def _save_studio_values(self): """Save studio override values.""" From 8cfe9bb270e2859c09a0ec17554b11aac4860a87 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:04:04 +0200 Subject: [PATCH 024/115] added first dynamic_schema item in schemas --- .../entities/schemas/projects_schema/schema_main.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index 4a8a9d496e..058ff492f3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -121,6 +121,10 @@ { "type": "schema", "name": "schema_project_unreal" + }, + { + "type": "dynamic_schema", + "name": "project_settings/global" } ] } From 1aa3d42704813ce67722f3053bd1a9462a90247c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 13:06:43 +0200 Subject: [PATCH 025/115] reset defaults on save defaults --- openpype/settings/entities/root_entities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 6f444d5394..78e8aad47f 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -31,6 +31,7 @@ from openpype.settings.lib import ( DEFAULTS_DIR, get_default_settings, + reset_default_settings, get_studio_system_settings_overrides, save_studio_settings, @@ -381,6 +382,7 @@ class RootEntity(BaseItemEntity): if self._override_state is OverrideState.DEFAULTS: self._save_default_values() + reset_default_settings() elif self._override_state is OverrideState.STUDIO: self._save_studio_values() From b4a3038623961e77c25d6784341370d21cbc08bb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 13 Aug 2021 18:02:12 +0200 Subject: [PATCH 026/115] add `--validate-version` and `--headless` --- igniter/__init__.py | 3 + igniter/bootstrap_repos.py | 124 +++++++++++++++++++++++- openpype/cli.py | 2 + start.py | 73 ++++++++++++-- website/docs/admin_openpype_commands.md | 3 + website/docs/admin_use.md | 13 +++ 6 files changed, 211 insertions(+), 7 deletions(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index 20bf9be106..73e315d88a 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -12,6 +12,9 @@ from .version import __version__ as version def open_dialog(): """Show Igniter dialog.""" + if os.getenv("OPENPYPE_HEADLESS_MODE"): + print("!!! Can't open dialog in headless mode. Exiting.") + sys.exit(1) from Qt import QtWidgets, QtCore from .install_dialog import InstallDialog diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 8c081b8614..22f5e7d94c 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -9,6 +9,7 @@ import sys import tempfile from pathlib import Path from typing import Union, Callable, List, Tuple +import hashlib from zipfile import ZipFile, BadZipFile @@ -28,6 +29,25 @@ LOG_WARNING = 1 LOG_ERROR = 3 +def sha256sum(filename): + """Calculate sha256 for content of the file. + + Args: + filename (str): Path to file. + + Returns: + str: hex encoded sha256 + + """ + h = hashlib.sha256() + b = bytearray(128 * 1024) + mv = memoryview(b) + with open(filename, 'rb', buffering=0) as f: + for n in iter(lambda: f.readinto(mv), 0): + h.update(mv[:n]) + return h.hexdigest() + + class OpenPypeVersion(semver.VersionInfo): """Class for storing information about OpenPype version. @@ -261,7 +281,8 @@ class BootstrapRepos: self.live_repo_dir = Path(Path(__file__).parent / ".." / "repos") @staticmethod - def get_version_path_from_list(version: str, version_list: list) -> Path: + def get_version_path_from_list( + version: str, version_list: list) -> Union[Path, None]: """Get path for specific version in list of OpenPype versions. Args: @@ -275,6 +296,7 @@ class BootstrapRepos: for v in version_list: if str(v) == version: return v.path + return None @staticmethod def get_local_live_version() -> str: @@ -487,6 +509,7 @@ class BootstrapRepos: openpype_root = openpype_path.resolve() # generate list of filtered paths dir_filter = [openpype_root / f for f in self.openpype_filter] + checksums = [] file: Path for file in openpype_list: @@ -508,11 +531,110 @@ class BootstrapRepos: processed_path = file self._print(f"- processing {processed_path}") + checksums.append( + ( + sha256sum(file.as_posix()), + file.resolve().relative_to(openpype_root) + ) + ) zip_file.write(file, file.relative_to(openpype_root)) + checksums_str = "" + for c in checksums: + checksums_str += "{}:{}\n".format(c[0], c[1]) + zip_file.writestr("checksums", checksums_str) # test if zip is ok zip_file.testzip() self._progress_callback(100) + + def validate_openpype_version(self, path: Path) -> tuple: + """Validate version directory or zip file. + + This will load `checksums` file if present, calculate checksums + of existing files in given path and compare. It will also compare + lists of files together for missing files. + + Args: + path (Path): Path to OpenPype version to validate. + + Returns: + tuple(bool, str): with version validity as first item and string with + reason as second. + + """ + if not path.exists(): + return False, "Path doesn't exist" + + if path.is_file(): + return self._validate_zip(path) + return self._validate_dir(path) + + @staticmethod + def _validate_zip(path: Path) -> tuple: + """Validate content of zip file.""" + with ZipFile(path, "r") as zip_file: + # read checksums + try: + checksums_data = str(zip_file.read("checksums")) + except IOError: + # FIXME: This should be set to False sometimes in the future + return True, "Cannot read checksums for archive." + + # split it to the list of tuples + checksums = [ + tuple(line.split(":")) + for line in checksums_data.split("\n") if line + ] + + # calculate and compare checksums in the zip file + for file in checksums: + h = hashlib.sha256() + h.update(zip_file.read(file[1])) + if h.hexdigest() != file[0]: + return False, f"Invalid checksum on {file[1]}" + + # get list of files in zip minus `checksums` file itself + # and turn in to set to compare against list of files + # from checksum file. If difference exists, something is + # wrong + files_in_zip = zip_file.namelist() + files_in_zip.remove("checksums") + files_in_zip = set(files_in_zip) + files_in_checksum = set([file[1] for file in checksums]) + diff = files_in_zip.difference(files_in_checksum) + if diff: + return False, f"Missing files {diff}" + + return True, "All ok" + + @staticmethod + def _validate_dir(path: Path) -> tuple: + checksums_file = Path(path / "checksums") + if not checksums_file.exists(): + # FIXME: This should be set to False sometimes in the future + return True, "Cannot read checksums for archive." + checksums_data = checksums_file.read_text() + checksums = [ + tuple(line.split(":")) + for line in checksums_data.split("\n") if line + ] + files_in_dir = [ + file.relative_to(path).as_posix() + for file in path.iterdir() if file.is_file() + ] + files_in_dir.remove("checksums") + files_in_dir = set(files_in_dir) + files_in_checksum = set([file[1] for file in checksums]) + + for file in checksums: + current = sha256sum((path / file[1]).as_posix()) + if file[0] != current: + return False, f"Invalid checksum on {file[1]}" + diff = files_in_dir.difference(files_in_checksum) + if diff: + return False, f"Missing files {diff}" + + return True, "All ok" @staticmethod def add_paths_from_archive(archive: Path) -> None: diff --git a/openpype/cli.py b/openpype/cli.py index ec5b04c468..be14a8aa7d 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -18,6 +18,8 @@ from .pype_commands import PypeCommands @click.option("--list-versions", is_flag=True, expose_value=False, help=("list all detected versions. Use With `--use-staging " "to list staging versions.")) +@click.option("--validate-version", + help="validate given version integrity") def main(ctx): """Pype is main command serving as entry point to pipeline system. diff --git a/start.py b/start.py index 6473a926d0..ca4b2835bb 100644 --- a/start.py +++ b/start.py @@ -179,8 +179,10 @@ else: ssl_cert_file = certifi.where() os.environ["SSL_CERT_FILE"] = ssl_cert_file +if "--headless" in sys.argv: + os.environ["OPENPYPE_HEADLESS_MODE"] = "1" -import igniter # noqa: E402 +import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( get_openpype_path_from_db, @@ -343,7 +345,7 @@ def _process_arguments() -> tuple: # check for `--use-version=3.0.0` argument and `--use-staging` use_version = None use_staging = False - print_versions = False + commands = [] for arg in sys.argv: if arg == "--use-version": _print("!!! Please use option --use-version like:") @@ -366,12 +368,30 @@ def _process_arguments() -> tuple: " proper version string.")) sys.exit(1) + if arg == "--validate-version": + _print("!!! Please use option --validate-version like:") + _print(" --validate-version=3.0.0") + sys.exit(1) + + if arg.startswith("--validate-version="): + m = re.search( + r"--validate-version=(?P\d+\.\d+\.\d+(?:\S*)?)", arg) + if m and m.group('version'): + use_version = m.group('version') + sys.argv.remove(arg) + commands.append("validate") + else: + _print("!!! Requested version isn't in correct format.") + _print((" Use --list-versions to find out" + " proper version string.")) + sys.exit(1) + if "--use-staging" in sys.argv: use_staging = True sys.argv.remove("--use-staging") if "--list-versions" in sys.argv: - print_versions = True + commands.append("print_versions") sys.argv.remove("--list-versions") # handle igniter @@ -389,7 +409,7 @@ def _process_arguments() -> tuple: sys.argv.pop(idx) sys.argv.insert(idx, "tray") - return use_version, use_staging, print_versions + return use_version, use_staging, commands def _determine_mongodb() -> str: @@ -738,7 +758,7 @@ def boot(): # Process arguments # ------------------------------------------------------------------------ - use_version, use_staging, print_versions = _process_arguments() + use_version, use_staging, commands = _process_arguments() if os.getenv("OPENPYPE_VERSION"): if use_version: @@ -766,13 +786,47 @@ def boot(): # Get openpype path from database and set it to environment so openpype can # find its versions there and bootstrap them. openpype_path = get_openpype_path_from_db(openpype_mongo) + + if getattr(sys, 'frozen', False): + local_version = bootstrap.get_version(Path(OPENPYPE_ROOT)) + else: + local_version = bootstrap.get_local_live_version() + + if "validate" in commands: + _print(f">>> Validating version [ {use_version} ]") + openpype_versions = bootstrap.find_openpype(include_zips=True, + staging=True) + openpype_versions += bootstrap.find_openpype(include_zips=True, + staging=False) + + v: OpenPypeVersion + found = [v for v in openpype_versions if str(v) == use_version] + if not found: + _print(f"!!! Version [ {use_version} ] not found.") + list_versions(openpype_versions, local_version) + sys.exit(1) + + # print result + result = bootstrap.validate_openpype_version( + bootstrap.get_version_path_from_list( + use_version, openpype_versions)) + + _print("{}{}".format( + ">>> " if result[0] else "!!! ", + bootstrap.validate_openpype_version( + bootstrap.get_version_path_from_list(use_version, openpype_versions) + )[1]) + ) + sys.exit(1) + + if not openpype_path: _print("*** Cannot get OpenPype path from database.") if not os.getenv("OPENPYPE_PATH") and openpype_path: os.environ["OPENPYPE_PATH"] = openpype_path - if print_versions: + if "print_versions" in commands: if not use_staging: _print("--- This will list only non-staging versions detected.") _print(" To see staging versions, use --use-staging argument.") @@ -803,6 +857,13 @@ def boot(): # no version to run _print(f"!!! {e}") sys.exit(1) + # validate version + _print(f">>> Validating version [ {str(version_path)} ]") + result = bootstrap.validate_openpype_version(version_path) + if not result[0]: + _print(f"!!! Invalid version: {result[1]}") + sys.exit(1) + _print(f"--- version is valid") else: version_path = _bootstrap_from_code(use_version, use_staging) diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 1a91e2e7fe..d6ccc883b0 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -18,11 +18,14 @@ Running OpenPype without any commands will default to `tray`. ```shell openpype_console --use-version=3.0.0-foo+bar ``` +`--headless` - to run OpenPype in headless mode (without using graphical UI) `--use-staging` - to use staging versions of OpenPype. `--list-versions [--use-staging]` - to list available versions. +`--validate-version` to validate integrity of given version + For more information [see here](admin_use#run-openpype). ## Commands diff --git a/website/docs/admin_use.md b/website/docs/admin_use.md index 4ad08a0174..178241ad19 100644 --- a/website/docs/admin_use.md +++ b/website/docs/admin_use.md @@ -56,6 +56,19 @@ openpype_console --list-versions You can add `--use-staging` to list staging versions. ::: +If you want to validate integrity of some available version, you can use: + +```shell +openpype_console --validate-version=3.3.0 +``` + +This will go through the version and validate file content against sha 256 hashes +stored in `checksums` file. + +:::tip Headless mode +Add `--headless` to run OpenPype without graphical UI (useful on server or on automated tasks, etc.) +::: + ### Details When you run OpenPype from executable, few check are made: From b6d831045796656f4bbd4ed98fbe256d22704295 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 13 Aug 2021 18:18:57 +0200 Subject: [PATCH 027/115] hound fixes, checks for missing files --- igniter/bootstrap_repos.py | 19 +++++++++++++------ start.py | 8 ++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 22f5e7d94c..535bb723bc 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -546,7 +546,7 @@ class BootstrapRepos: # test if zip is ok zip_file.testzip() self._progress_callback(100) - + def validate_openpype_version(self, path: Path) -> tuple: """Validate version directory or zip file. @@ -558,13 +558,13 @@ class BootstrapRepos: path (Path): Path to OpenPype version to validate. Returns: - tuple(bool, str): with version validity as first item and string with - reason as second. + tuple(bool, str): with version validity as first item + and string with reason as second. """ if not path.exists(): return False, "Path doesn't exist" - + if path.is_file(): return self._validate_zip(path) return self._validate_dir(path) @@ -589,7 +589,10 @@ class BootstrapRepos: # calculate and compare checksums in the zip file for file in checksums: h = hashlib.sha256() - h.update(zip_file.read(file[1])) + try: + h.update(zip_file.read(file[1])) + except FileNotFoundError: + return False, f"Missing file [ {file[1]} ]" if h.hexdigest() != file[0]: return False, f"Invalid checksum on {file[1]}" @@ -627,7 +630,11 @@ class BootstrapRepos: files_in_checksum = set([file[1] for file in checksums]) for file in checksums: - current = sha256sum((path / file[1]).as_posix()) + try: + current = sha256sum((path / file[1]).as_posix()) + except FileNotFoundError: + return False, f"Missing file [ {file[1]} ]" + if file[0] != current: return False, f"Invalid checksum on {file[1]}" diff = files_in_dir.difference(files_in_checksum) diff --git a/start.py b/start.py index ca4b2835bb..a5f662d39b 100644 --- a/start.py +++ b/start.py @@ -182,7 +182,7 @@ else: if "--headless" in sys.argv: os.environ["OPENPYPE_HEADLESS_MODE"] = "1" -import igniter # noqa: E402 +import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( get_openpype_path_from_db, @@ -797,8 +797,7 @@ def boot(): openpype_versions = bootstrap.find_openpype(include_zips=True, staging=True) openpype_versions += bootstrap.find_openpype(include_zips=True, - staging=False) - + staging=False) v: OpenPypeVersion found = [v for v in openpype_versions if str(v) == use_version] if not found: @@ -814,7 +813,8 @@ def boot(): _print("{}{}".format( ">>> " if result[0] else "!!! ", bootstrap.validate_openpype_version( - bootstrap.get_version_path_from_list(use_version, openpype_versions) + bootstrap.get_version_path_from_list( + use_version, openpype_versions) )[1]) ) sys.exit(1) From df310da1411ef10331daad7a0271efe1cb7cf429 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Aug 2021 17:57:28 +0200 Subject: [PATCH 028/115] add update dialog --- igniter/__init__.py | 23 +++++ igniter/bootstrap_repos.py | 6 ++ igniter/update_thread.py | 61 +++++++++++++ igniter/update_window.py | 173 +++++++++++++++++++++++++++++++++++++ openpype/cli.py | 2 +- start.py | 12 ++- 6 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 igniter/update_thread.py create mode 100644 igniter/update_window.py diff --git a/igniter/__init__.py b/igniter/__init__.py index 73e315d88a..defd45e233 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -31,8 +31,31 @@ def open_dialog(): return d.result() +def open_update_window(openpype_version): + """Open update window.""" + if os.getenv("OPENPYPE_HEADLESS_MODE"): + print("!!! Can't open dialog in headless mode. Exiting.") + sys.exit(1) + from Qt import QtWidgets, QtCore + from .update_window import UpdateWindow + + scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None) + if scale_attr is not None: + QtWidgets.QApplication.setAttribute(scale_attr) + + app = QtWidgets.QApplication(sys.argv) + + d = UpdateWindow(version=openpype_version) + d.open() + + app.exec_() + version_path = d.get_version_path() + return version_path + + __all__ = [ "BootstrapRepos", "open_dialog", + "open_update_window", "version" ] diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 535bb723bc..c527de0066 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -966,6 +966,7 @@ class BootstrapRepos: # test if destination directory already exist, if so lets delete it. if destination.exists() and force: + self._print("removing existing directory") try: shutil.rmtree(destination) except OSError as e: @@ -975,6 +976,7 @@ class BootstrapRepos: raise OpenPypeVersionIOError( f"cannot remove existing {destination}") from e elif destination.exists() and not force: + self._print("destination directory already exists") raise OpenPypeVersionExists(f"{destination} already exist.") else: # create destination parent directories even if they don't exist. @@ -984,6 +986,7 @@ class BootstrapRepos: if openpype_version.path.is_dir(): # create zip inside temporary directory. self._print("Creating zip from directory ...") + self._progress_callback(0) with tempfile.TemporaryDirectory() as temp_dir: temp_zip = \ Path(temp_dir) / f"openpype-v{openpype_version}.zip" @@ -1009,13 +1012,16 @@ class BootstrapRepos: raise OpenPypeVersionInvalid("Invalid file format") if not self.is_inside_user_data(openpype_version.path): + self._progress_callback(35) openpype_version.path = self._copy_zip( openpype_version.path, destination) # extract zip there self._print("extracting zip to destination ...") with ZipFile(openpype_version.path, "r") as zip_ref: + self._progress_callback(75) zip_ref.extractall(destination) + self._progress_callback(100) return destination diff --git a/igniter/update_thread.py b/igniter/update_thread.py new file mode 100644 index 0000000000..f4fc729faf --- /dev/null +++ b/igniter/update_thread.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""Working thread for update.""" +from Qt.QtCore import QThread, Signal, QObject # noqa + +from .bootstrap_repos import ( + BootstrapRepos, + OpenPypeVersion +) + + +class UpdateThread(QThread): + """Install Worker thread. + + This class takes care of finding OpenPype version on user entered path + (or loading this path from database). If nothing is entered by user, + OpenPype will create its zip files from repositories that comes with it. + + If path contains plain repositories, they are zipped and installed to + user data dir. + + """ + progress = Signal(int) + message = Signal((str, bool)) + + def __init__(self, parent=None): + self._result = None + self._openpype_version = None + QThread.__init__(self, parent) + + def set_version(self, openpype_version: OpenPypeVersion): + self._openpype_version = openpype_version + + def result(self): + """Result of finished installation.""" + return self._result + + def _set_result(self, value): + if self._result is not None: + raise AssertionError("BUG: Result was set more than once!") + self._result = value + + def run(self): + """Thread entry point. + + Using :class:`BootstrapRepos` to either install OpenPype as zip files + or copy them from location specified by user or retrieved from + database. + """ + bs = BootstrapRepos( + progress_callback=self.set_progress, message=self.message) + version_path = bs.install_version(self._openpype_version) + self._set_result(version_path) + + def set_progress(self, progress: int) -> None: + """Helper to set progress bar. + + Args: + progress (int): Progress in percents. + + """ + self.progress.emit(progress) diff --git a/igniter/update_window.py b/igniter/update_window.py new file mode 100644 index 0000000000..2edb3f2c6b --- /dev/null +++ b/igniter/update_window.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +"""Progress window to show when OpenPype is updating/installing locally.""" +import os +import sys +from pathlib import Path +from .update_thread import UpdateThread +from Qt import QtCore, QtGui, QtWidgets # noqa +from .bootstrap_repos import OpenPypeVersion + + +def load_stylesheet(path: str = None) -> str: + """Load css style sheet. + + Args: + path (str, optional): Path to stylesheet. If none, `stylesheet.css` from + current package's path is used. + Returns: + str: content of the stylesheet + + """ + if path: + stylesheet_path = Path(path) + else: + stylesheet_path = Path(os.path.dirname(__file__)) / "stylesheet.css" + + return stylesheet_path.read_text() + + +class NiceProgressBar(QtWidgets.QProgressBar): + def __init__(self, parent=None): + super(NiceProgressBar, self).__init__(parent) + self._real_value = 0 + + def setValue(self, value): + self._real_value = value + if value != 0 and value < 11: + value = 11 + + super(NiceProgressBar, self).setValue(value) + + def value(self): + return self._real_value + + def text(self): + return "{} %".format(self._real_value) + + +class UpdateWindow(QtWidgets.QDialog): + """OpenPype update window.""" + + _width = 500 + _height = 100 + + def __init__(self, version: OpenPypeVersion, parent=None): + super(UpdateWindow, self).__init__(parent) + self._openpype_version = version + self._result_version_path = None + + self.setWindowTitle( + f"OpenPype is updating ..." + ) + self.setModal(True) + self.setWindowFlags( + QtCore.Qt.WindowMinimizeButtonHint + ) + + current_dir = os.path.dirname(os.path.abspath(__file__)) + roboto_font_path = os.path.join(current_dir, "RobotoMono-Regular.ttf") + poppins_font_path = os.path.join(current_dir, "Poppins") + icon_path = os.path.join(current_dir, "openpype_icon.png") + + # Install roboto font + QtGui.QFontDatabase.addApplicationFont(roboto_font_path) + for filename in os.listdir(poppins_font_path): + if os.path.splitext(filename)[1] == ".ttf": + QtGui.QFontDatabase.addApplicationFont(filename) + + # Load logo + pixmap_openpype_logo = QtGui.QPixmap(icon_path) + # Set logo as icon of window + self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo)) + + self._pixmap_openpype_logo = pixmap_openpype_logo + + self._update_thread = None + + self.resize(QtCore.QSize(self._width, self._height)) + self._init_ui() + + # Set stylesheet + self.setStyleSheet(load_stylesheet()) + self._run_update() + + def _init_ui(self): + + # Main info + # -------------------------------------------------------------------- + main_label = QtWidgets.QLabel( + f"OpenPype is updating to {self._openpype_version}", self) + main_label.setWordWrap(True) + main_label.setObjectName("MainLabel") + + # Progress bar + # -------------------------------------------------------------------- + progress_bar = NiceProgressBar(self) + progress_bar.setAlignment(QtCore.Qt.AlignCenter) + progress_bar.setTextVisible(False) + + # add all to main + main = QtWidgets.QVBoxLayout(self) + main.addSpacing(15) + main.addWidget(main_label, 0) + main.addSpacing(15) + main.addWidget(progress_bar, 0) + main.addSpacing(15) + + self._progress_bar = progress_bar + + def _run_update(self): + """Start install process. + + This will once again validate entered path and mongo if ok, start + working thread that will do actual job. + """ + # Check if install thread is not already running + if self._update_thread and self._update_thread.isRunning(): + return + self._progress_bar.setRange(0, 0) + update_thread = UpdateThread(self) + update_thread.set_version(self._openpype_version) + update_thread.message.connect(self.update_console) + update_thread.progress.connect(self._update_progress) + update_thread.finished.connect(self._installation_finished) + + self._update_thread = update_thread + + update_thread.start() + + def get_version_path(self): + return self._result_version_path + + def _installation_finished(self): + status = self._update_thread.result() + self._result_version_path = status + self._progress_bar.setRange(0, 1) + self._update_progress(100) + QtWidgets.QApplication.processEvents() + self.done(0) + + def _update_progress(self, progress: int): + # not updating progress as we are not able to determine it + # correctly now. Progress bar is set to un-deterministic mode + # until we are able to get progress in better way. + """ + self._progress_bar.setRange(0, 0) + self._progress_bar.setValue(progress) + text_visible = self._progress_bar.isTextVisible() + if progress == 0: + if text_visible: + self._progress_bar.setTextVisible(False) + elif not text_visible: + self._progress_bar.setTextVisible(True) + """ + return + + def update_console(self, msg: str, error: bool = False) -> None: + """Display message in console. + + Args: + msg (str): message. + error (bool): if True, print it red. + """ + print(msg) diff --git a/openpype/cli.py b/openpype/cli.py index be14a8aa7d..632c3d3386 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -18,7 +18,7 @@ from .pype_commands import PypeCommands @click.option("--list-versions", is_flag=True, expose_value=False, help=("list all detected versions. Use With `--use-staging " "to list staging versions.")) -@click.option("--validate-version", +@click.option("--validate-version", expose_value=False, help="validate given version integrity") def main(ctx): """Pype is main command serving as entry point to pipeline system. diff --git a/start.py b/start.py index a5f662d39b..ca48fdf3b7 100644 --- a/start.py +++ b/start.py @@ -610,8 +610,16 @@ def _find_frozen_openpype(use_version: str = None, if not is_inside: # install latest version to user data dir - version_path = bootstrap.install_version( - openpype_version, force=True) + if not os.getenv("OPENPYPE_HEADLESS"): + import igniter + version_path = igniter.open_update_window(openpype_version) + else: + version_path = bootstrap.install_version( + openpype_version, force=True) + + openpype_version.path = version_path + _initialize_environment(openpype_version) + return openpype_version.path if openpype_version.path.is_file(): _print(">>> Extracting zip file ...") From 2b11e589c5610d5d9ab0232ed8a17bf0ca295949 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Aug 2021 18:06:57 +0200 Subject: [PATCH 029/115] handle igniter dialog --- igniter/update_window.py | 5 ++--- start.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/igniter/update_window.py b/igniter/update_window.py index 2edb3f2c6b..a49a84cfee 100644 --- a/igniter/update_window.py +++ b/igniter/update_window.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Progress window to show when OpenPype is updating/installing locally.""" import os -import sys from pathlib import Path from .update_thread import UpdateThread from Qt import QtCore, QtGui, QtWidgets # noqa @@ -12,8 +11,8 @@ def load_stylesheet(path: str = None) -> str: """Load css style sheet. Args: - path (str, optional): Path to stylesheet. If none, `stylesheet.css` from - current package's path is used. + path (str, optional): Path to stylesheet. If none, `stylesheet.css` + from current package's path is used. Returns: str: content of the stylesheet diff --git a/start.py b/start.py index ca48fdf3b7..27dc105394 100644 --- a/start.py +++ b/start.py @@ -397,6 +397,9 @@ def _process_arguments() -> tuple: # handle igniter # this is helper to run igniter before anything else if "igniter" in sys.argv: + if os.getenv("OPENPYPE_HEADLESS"): + _print("!!! Cannot open Igniter dialog in headless mode.") + sys.exit(1) import igniter return_code = igniter.open_dialog() @@ -444,6 +447,11 @@ def _determine_mongodb() -> str: if not openpype_mongo: _print("*** No DB connection string specified.") + if os.getenv("OPENPYPE_HEADLESS"): + _print("!!! Cannot open Igniter dialog in headless mode.") + _print( + "!!! Please use `OPENPYPE_MONGO` to specify server address.") + sys.exit(1) _print("--- launching setup UI ...") result = igniter.open_dialog() @@ -547,6 +555,9 @@ def _find_frozen_openpype(use_version: str = None, except IndexError: # no OpenPype version found, run Igniter and ask for them. _print('*** No OpenPype versions found.') + if os.getenv("OPENPYPE_HEADLESS"): + _print("!!! Cannot open Igniter dialog in headless mode.") + sys.exit(1) _print("--- launching setup UI ...") import igniter return_code = igniter.open_dialog() From fcb2640c9492722171270b6328469c71dbbcf7c2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Aug 2021 18:12:38 +0200 Subject: [PATCH 030/115] fix env var name --- start.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/start.py b/start.py index 27dc105394..9e60d79f04 100644 --- a/start.py +++ b/start.py @@ -397,7 +397,7 @@ def _process_arguments() -> tuple: # handle igniter # this is helper to run igniter before anything else if "igniter" in sys.argv: - if os.getenv("OPENPYPE_HEADLESS"): + if os.getenv("OPENPYPE_HEADLESS_MODE"): _print("!!! Cannot open Igniter dialog in headless mode.") sys.exit(1) import igniter @@ -447,7 +447,7 @@ def _determine_mongodb() -> str: if not openpype_mongo: _print("*** No DB connection string specified.") - if os.getenv("OPENPYPE_HEADLESS"): + if os.getenv("OPENPYPE_HEADLESS_MODE"): _print("!!! Cannot open Igniter dialog in headless mode.") _print( "!!! Please use `OPENPYPE_MONGO` to specify server address.") @@ -555,7 +555,7 @@ def _find_frozen_openpype(use_version: str = None, except IndexError: # no OpenPype version found, run Igniter and ask for them. _print('*** No OpenPype versions found.') - if os.getenv("OPENPYPE_HEADLESS"): + if os.getenv("OPENPYPE_HEADLESS_MODE"): _print("!!! Cannot open Igniter dialog in headless mode.") sys.exit(1) _print("--- launching setup UI ...") @@ -621,7 +621,7 @@ def _find_frozen_openpype(use_version: str = None, if not is_inside: # install latest version to user data dir - if not os.getenv("OPENPYPE_HEADLESS"): + if not os.getenv("OPENPYPE_HEADLESS_MODE"): import igniter version_path = igniter.open_update_window(openpype_version) else: From 6b6877e76ed934b22bfc3b4206de7ed16984a52e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:52:49 +0200 Subject: [PATCH 031/115] fixed get_general_environments function --- openpype/settings/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index b5810deef4..9797458fd5 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -2,6 +2,7 @@ from .exceptions import ( SaveWarningExc ) from .lib import ( + get_general_environments, get_system_settings, get_project_settings, get_current_project_settings, @@ -18,6 +19,7 @@ from .entities import ( __all__ = ( "SaveWarningExc", + "get_general_environments", "get_system_settings", "get_project_settings", "get_current_project_settings", From cf811c7e0f56353af0a3a18e01593db310d1edce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:53:03 +0200 Subject: [PATCH 032/115] added addons_path key to settings --- .../settings/defaults/system_settings/modules.json | 5 +++++ .../schemas/system_schema/schema_modules.json | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 3a70b90590..12cca7ccf1 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -1,4 +1,9 @@ { + "addon_paths": { + "windows": [], + "darwin": [], + "linux": [] + }, "avalon": { "AVALON_TIMEOUT": 1000, "AVALON_THUMBNAIL_ROOT": { diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 75c08b2cd9..0e52cea69e 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -5,6 +5,18 @@ "collapsible": true, "is_file": true, "children": [ + { + "type": "path", + "key": "addon_paths", + "label": "OpenPype AddOn Paths", + "use_label_wrap": true, + "multiplatform": true, + "multipath": true, + "require_restart": true + }, + { + "type": "separator" + }, { "type": "dict", "key": "avalon", From b92621a270704bb8b61842d977d883ea230b304f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:53:19 +0200 Subject: [PATCH 033/115] don't crash if path does not exists --- openpype/modules/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 3d3d7ae6cb..e407a34606 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -165,6 +165,9 @@ def _load_interfaces(): os.path.join(get_default_modules_dir(), "interfaces.py") ) for dirpath in dirpaths: + if not os.path.exists(dirpath): + continue + for filename in os.listdir(dirpath): if filename in ("__pycache__", ): continue From bf1a5c85ccf4db4d3be2c99a7ff3f1c9d5cdf519 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:54:08 +0200 Subject: [PATCH 034/115] added get_dynamic_modules_dirs to be able get paths to openpype addons --- openpype/modules/base.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index e407a34606..a3269e99e9 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -115,11 +115,24 @@ def get_default_modules_dir(): return os.path.join(current_dir, "default_modules") +def get_dynamic_modules_dirs(): + output = [] + return output + + def get_module_dirs(): """List of paths where OpenPype modules can be found.""" - dirpaths = [ - get_default_modules_dir() - ] + _dirpaths = [] + _dirpaths.append(get_default_modules_dir()) + _dirpaths.extend(get_dynamic_modules_dirs()) + + dirpaths = [] + for path in _dirpaths: + if not path: + continue + normalized = os.path.normpath(path) + if normalized not in dirpaths: + dirpaths.append(normalized) return dirpaths From e3754a85a662dbcbaf2d06cc8285638b7ea9d7d2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 15:54:23 +0200 Subject: [PATCH 035/115] implemented logic of dynamic addons paths --- openpype/modules/base.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index a3269e99e9..d3b83e85b1 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -5,6 +5,7 @@ import sys import time import inspect import logging +import platform import threading import collections from uuid import uuid4 @@ -13,6 +14,7 @@ import six import openpype from openpype.settings import get_system_settings +from openpype.settings.lib import get_studio_system_settings_overrides from openpype.lib import PypeLogger @@ -117,6 +119,21 @@ def get_default_modules_dir(): def get_dynamic_modules_dirs(): output = [] + value = get_studio_system_settings_overrides() + for key in ("modules", "addon_paths", platform.system().lower()): + if key not in value: + return output + value = value[key] + + for path in value: + if not path: + continue + + try: + path = path.format(**os.environ) + except Exception: + pass + output.append(path) return output From 2495cffd509a51838fc1c8c5c77d05a007794322 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 16:44:58 +0200 Subject: [PATCH 036/115] don't crash whole openpype on broken addon/module --- openpype/modules/base.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index d3b83e85b1..9df9b3a97b 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -305,12 +305,19 @@ def _load_modules(): # TODO add more logic how to define if folder is module or not # - check manifest and content of manifest - if os.path.isdir(fullpath): - import_module_from_dirpath(dirpath, filename, modules_key) + try: + if os.path.isdir(fullpath): + import_module_from_dirpath(dirpath, filename, modules_key) - elif ext in (".py", ): - module = import_filepath(fullpath) - setattr(openpype_modules, basename, module) + elif ext in (".py", ): + module = import_filepath(fullpath) + setattr(openpype_modules, basename, module) + + except Exception: + log.error( + "Failed to import '{}'.".format(fullpath), + exc_info=True + ) class _OpenPypeInterfaceMeta(ABCMeta): From bd791c971985bd2229c256d8946f808733307597 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:08:44 +0200 Subject: [PATCH 037/115] moved few settings constants to constants.py --- openpype/settings/__init__.py | 25 +++++++++++++++++++++++++ openpype/settings/constants.py | 9 ++++++++- openpype/settings/entities/lib.py | 7 ++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 9797458fd5..0adb5db0bd 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -1,3 +1,16 @@ +from .constants import ( + GLOBAL_SETTINGS_KEY, + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, + PROJECT_ANATOMY_KEY, + LOCAL_SETTING_KEY, + + SCHEMA_KEY_SYSTEM_SETTINGS, + SCHEMA_KEY_PROJECT_SETTINGS, + + KEY_ALLOWED_SYMBOLS, + KEY_REGEX +) from .exceptions import ( SaveWarningExc ) @@ -17,6 +30,18 @@ from .entities import ( __all__ = ( + "GLOBAL_SETTINGS_KEY", + "SYSTEM_SETTINGS_KEY", + "PROJECT_SETTINGS_KEY", + "PROJECT_ANATOMY_KEY", + "LOCAL_SETTING_KEY", + + "SCHEMA_KEY_SYSTEM_SETTINGS", + "SCHEMA_KEY_PROJECT_SETTINGS", + + "KEY_ALLOWED_SYMBOLS", + "KEY_REGEX", + "SaveWarningExc", "get_general_environments", diff --git a/openpype/settings/constants.py b/openpype/settings/constants.py index a53e88a91e..2ea19ead4b 100644 --- a/openpype/settings/constants.py +++ b/openpype/settings/constants.py @@ -14,13 +14,17 @@ METADATA_KEYS = ( M_DYNAMIC_KEY_LABEL ) -# File where studio's system overrides are stored +# Keys where studio's system overrides are stored GLOBAL_SETTINGS_KEY = "global_settings" SYSTEM_SETTINGS_KEY = "system_settings" PROJECT_SETTINGS_KEY = "project_settings" PROJECT_ANATOMY_KEY = "project_anatomy" LOCAL_SETTING_KEY = "local_settings" +# Schema hub names +SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" +SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" + DEFAULT_PROJECT_KEY = "__default_project__" KEY_ALLOWED_SYMBOLS = "a-zA-Z0-9-_ " @@ -39,6 +43,9 @@ __all__ = ( "PROJECT_ANATOMY_KEY", "LOCAL_SETTING_KEY", + "SCHEMA_KEY_SYSTEM_SETTINGS", + "SCHEMA_KEY_PROJECT_SETTINGS", + "DEFAULT_PROJECT_KEY", "KEY_ALLOWED_SYMBOLS", diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index f7036726d2..d4b0e10864 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -11,6 +11,10 @@ from .exceptions import ( SchemaDuplicatedEnvGroupKeys ) +from openpype.settings.constants import ( + SCHEMA_KEY_SYSTEM_SETTINGS, + SCHEMA_KEY_PROJECT_SETTINGS +) try: STRING_TYPE = basestring except Exception: @@ -25,9 +29,6 @@ TEMPLATE_METADATA_KEYS = ( DEFAULT_VALUES_KEY, ) -SCHEMA_KEY_SYSTEM_SETTINGS = "system_schema" -SCHEMA_KEY_PROJECT_SETTINGS = "projects_schema" - SCHEMA_EXTEND_TYPES = ( "schema", "template", "schema_template", "dynamic_schema" ) From 2706c7759f6ed01aecb7a205f4cfc806aa22b7d3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:15:06 +0200 Subject: [PATCH 038/115] a littlebit safer return value check --- openpype/settings/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index d7684082f3..60ed54bd4a 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -337,7 +337,9 @@ def _get_default_settings(): module_settings_defs = get_module_settings_defs() for module_settings_def_cls in module_settings_defs: module_settings_def = module_settings_def_cls() - system_defaults = module_settings_def.get_system_defaults() + system_defaults = module_settings_def.get_defaults( + SYSTEM_SETTINGS_KEY + ) or {} for path, value in system_defaults.items(): if not path: continue @@ -349,7 +351,9 @@ def _get_default_settings(): subdict = subdict[key] subdict[last_key] = value - project_defaults = module_settings_def.get_project_defaults() + project_defaults = module_settings_def.get_defaults( + PROJECT_SETTINGS_KEY + ) or {} for path, value in project_defaults.items(): if not path: continue From 735f4b847b7e990c8e6c0e22ed45a5d6d4c6c829 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:15:29 +0200 Subject: [PATCH 039/115] added mapping of schema hub key to top key value --- openpype/settings/entities/lib.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index d4b0e10864..f207322dee 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -12,6 +12,8 @@ from .exceptions import ( ) from openpype.settings.constants import ( + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, SCHEMA_KEY_SYSTEM_SETTINGS, SCHEMA_KEY_PROJECT_SETTINGS ) @@ -734,6 +736,12 @@ class SchemasHub: class DynamicSchemaValueCollector: + # Map schema hub type to store keys + schema_hub_type_map = { + SCHEMA_KEY_SYSTEM_SETTINGS: SYSTEM_SETTINGS_KEY, + SCHEMA_KEY_PROJECT_SETTINGS: PROJECT_SETTINGS_KEY + } + def __init__(self, schema_hub): self._schema_hub = schema_hub self._dynamic_entities = [] @@ -756,7 +764,7 @@ class DynamicSchemaValueCollector: schema_def = self._schema_hub.get_dynamic_modules_settings_defs( schema_def_id ) - if self._schema_hub.schema_type == SCHEMA_KEY_SYSTEM_SETTINGS: - schema_def.save_system_defaults(schema_def_value) - elif self._schema_hub.schema_type == SCHEMA_KEY_PROJECT_SETTINGS: - schema_def.save_project_defaults(schema_def_value) + top_key = self.schema_hub_type_map.get( + self._schema_hub.schema_type + ) + schema_def.save_defaults(top_key, schema_def_value) From f30253697127285c21650dc400f81de577f24074 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:15:46 +0200 Subject: [PATCH 040/115] eliminated methods in ModuleSettingsDef --- openpype/modules/base.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 9df9b3a97b..ce555c6bbf 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1025,17 +1025,9 @@ class ModuleSettingsDef: pass @abstractmethod - def save_system_defaults(self, data): + def get_defaults(self, top_key): pass @abstractmethod - def save_project_defaults(self, data): - pass - - @abstractmethod - def get_system_defaults(self): - pass - - @abstractmethod - def get_project_defaults(self): + def save_defaults(self, top_key, data): pass From c16cee6f810ecd1cd0a45b99cf77079e83c6d51f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:47:37 +0200 Subject: [PATCH 041/115] added few docstrings --- openpype/modules/base.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index ce555c6bbf..9972126136 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1008,26 +1008,63 @@ def get_module_settings_defs(): @six.add_metaclass(ABCMeta) class ModuleSettingsDef: + """Definition of settings for OpenPype module or AddOn.""" _id = None @property def id(self): + """ID created on initialization. + + ID should be per created object. Helps to store objects. + """ if self._id is None: self._id = uuid4() return self._id @abstractmethod def get_settings_schemas(self, schema_type): + """Setting schemas for passed schema type. + + These are main schemas by dynamic schema keys. If they're using + sub schemas or templates they should be loaded with + `get_dynamic_schemas`. + + Returns: + dict: Schema by `dynamic_schema` keys. + """ pass @abstractmethod def get_dynamic_schemas(self, schema_type): + """Settings schemas and templates that can be used anywhere. + + It is recommended to add prefix specific for addon/module to keys + (e.g. "my_addon/real_schema_name"). + + Returns: + dict: Schemas and templates by their keys. + """ pass @abstractmethod def get_defaults(self, top_key): + """Default values for passed top key. + + Top keys are (currently) "system_settings" or "project_settings". + + Should return exactly what was passed with `save_defaults`. + + Returns: + dict: Default values by path to first key in OpenPype defaults. + """ pass @abstractmethod def save_defaults(self, top_key, data): + """Save default values for passed top key. + + Top keys are (currently) "system_settings" or "project_settings". + + Passed data are by path to first key defined in main schemas. + """ pass From 60ff21534219da5eeada4c69873ae0baff4b71a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 17:54:04 +0200 Subject: [PATCH 042/115] added few infor to readme --- openpype/settings/entities/schemas/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 2034d4e463..a34732fbad 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -112,6 +112,22 @@ ``` - It is possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `"__default_values__"` and value as dictionary with default key: values (as in example above). +### dynamic_schema +- dynamic templates that can be defined by class of `ModuleSettingsDef` +- example: +``` +{ + "type": "dynamic_schema", + "name": "project_settings/global" +} +``` +- all valid `ModuleSettingsDef` classes where calling of `get_settings_schemas` + will return dictionary where is key "project_settings/global" with schemas + will extend and replace this item +- works almost the same way as templates + - one item can be replaced by multiple items (or by 0 items) +- goal is to dynamically loaded settings of OpenPype addons without having + their schemas or default values in main repository ## Basic Dictionary inputs - these inputs wraps another inputs into {key: value} relation From b869f2ab82657d24af093554654c48538a177be6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Aug 2021 18:00:18 +0200 Subject: [PATCH 043/115] added few more docstrings --- openpype/modules/base.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 9972126136..c8cc911ca6 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -118,6 +118,18 @@ def get_default_modules_dir(): def get_dynamic_modules_dirs(): + """Possible paths to OpenPype Addons of Modules. + + Paths are loaded from studio settings under: + `modules -> addon_paths -> {platform name}` + + Path may contain environment variable as a formatting string. + + They are not validated or checked their existence. + + Returns: + list: Paths loaded from studio overrides. + """ output = [] value = get_studio_system_settings_overrides() for key in ("modules", "addon_paths", platform.system().lower()): @@ -963,6 +975,17 @@ class TrayModulesManager(ModulesManager): def get_module_settings_defs(): + """Check loaded addons/modules for existence of thei settings definition. + + Check if OpenPype addon/module as python module has class that inherit + from `ModuleSettingsDef` in python module variables (imported + in `__init__py`). + + Returns: + list: All valid and not abstract settings definitions from imported + openpype addons and modules. + """ + # Make sure modules are loaded load_modules() import openpype_modules From e5456fe55b1b22c5b8f97dd33201eaf5e28f705e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 26 Aug 2021 19:40:05 +0200 Subject: [PATCH 044/115] initial commit of "NiceSlide" widget --- openpype/widgets/sliders.py | 138 ++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 openpype/widgets/sliders.py diff --git a/openpype/widgets/sliders.py b/openpype/widgets/sliders.py new file mode 100644 index 0000000000..2f26c3eb97 --- /dev/null +++ b/openpype/widgets/sliders.py @@ -0,0 +1,138 @@ +from Qt import QtWidgets, QtCore, QtGui + + +class NiceSlider(QtWidgets.QSlider): + def __init__(self, *args, **kwargs): + super(NiceSlider, self).__init__(*args, **kwargs) + self._mouse_clicked = False + self._handle_size = 0 + + self._bg_brush = QtGui.QBrush(QtGui.QColor("#21252B")) + self._fill_brush = QtGui.QBrush(QtGui.QColor("#5cadd6")) + + def mousePressEvent(self, event): + self._mouse_clicked = True + if event.button() == QtCore.Qt.LeftButton: + self._set_value_to_pos(event.pos()) + return event.accept() + return super(NiceSlider, self).mousePressEvent(event) + + def mouseMoveEvent(self, event): + if self._mouse_clicked: + self._set_value_to_pos(event.pos()) + + super(NiceSlider, self).mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self._mouse_clicked = True + super(NiceSlider, self).mouseReleaseEvent(event) + + def _set_value_to_pos(self, pos): + if self.orientation() == QtCore.Qt.Horizontal: + self._set_value_to_pos_x(pos.x()) + else: + self._set_value_to_pos_y(pos.y()) + + def _set_value_to_pos_x(self, pos_x): + _range = self.maximum() - self.minimum() + handle_size = self._handle_size + half_handle = handle_size / 2 + pos_x -= half_handle + width = self.width() - handle_size + value = ((_range * pos_x) / width) + self.minimum() + self.setValue(value) + + def _set_value_to_pos_y(self, pos_y): + _range = self.maximum() - self.minimum() + handle_size = self._handle_size + half_handle = handle_size / 2 + pos_y = self.height() - pos_y - half_handle + height = self.height() - handle_size + value = (_range * pos_y / height) + self.minimum() + self.setValue(value) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + opt = QtWidgets.QStyleOptionSlider() + self.initStyleOption(opt) + + painter.fillRect(event.rect(), QtCore.Qt.transparent) + + painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + + horizontal = self.orientation() == QtCore.Qt.Horizontal + + rect = self.style().subControlRect( + QtWidgets.QStyle.CC_Slider, + opt, + QtWidgets.QStyle.SC_SliderGroove, + self + ) + + _range = self.maximum() - self.minimum() + if horizontal: + _handle_half = rect.height() / 2 + _handle_size = _handle_half * 2 + width = rect.width() - _handle_size + pos_x = ((width / _range) * self.value()) + pos_y = rect.center().y() - _handle_half + 1 + else: + _handle_half = rect.width() / 2 + _handle_size = _handle_half * 2 + height = rect.height() - _handle_size + pos_x = rect.center().x() - _handle_half + 1 + pos_y = height - ((height / _range) * self.value()) + + handle_rect = QtCore.QRect( + pos_x, pos_y, _handle_size, _handle_size + ) + + self._handle_size = _handle_size + _offset = 2 + _size = _handle_size - _offset + if horizontal: + if rect.height() > _size: + new_rect = QtCore.QRect(0, 0, rect.width(), _size) + center_point = QtCore.QPoint( + rect.center().x(), handle_rect.center().y() + ) + new_rect.moveCenter(center_point) + rect = new_rect + + ratio = rect.height() / 2 + fill_rect = QtCore.QRect( + rect.x(), + rect.y(), + handle_rect.right() - rect.x(), + rect.height() + ) + + else: + if rect.width() > _size: + new_rect = QtCore.QRect(0, 0, _size, rect.height()) + center_point = QtCore.QPoint( + handle_rect.center().x(), rect.center().y() + ) + new_rect.moveCenter(center_point) + rect = new_rect + + ratio = rect.width() / 2 + fill_rect = QtCore.QRect( + rect.x(), + handle_rect.y(), + rect.width(), + rect.height() - handle_rect.y(), + ) + + painter.save() + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(self._bg_brush) + painter.drawRoundedRect(rect, ratio, ratio) + + painter.setBrush(self._fill_brush) + painter.drawRoundedRect(fill_rect, ratio, ratio) + + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(self._fill_brush) + painter.drawEllipse(handle_rect) + painter.restore() From 4c55040a58c9b68ebf1a804b7b2df84fb45b9c16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 16:55:10 +0200 Subject: [PATCH 045/115] enhanced `ModuleSettingsDef` to split each method into 2 separated abstract methods --- openpype/modules/__init__.py | 2 + openpype/modules/base.py | 124 ++++++++++++++++++++++++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 6169f99f77..6b3c0dc3a6 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -9,6 +9,7 @@ from .base import ( ModulesManager, TrayModulesManager, + BaseModuleSettingsDef, ModuleSettingsDef, get_module_settings_defs ) @@ -24,6 +25,7 @@ __all__ = ( "ModulesManager", "TrayModulesManager", + "BaseModuleSettingsDef", "ModuleSettingsDef", "get_module_settings_defs" ) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index c8cc911ca6..66f962526f 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -13,8 +13,17 @@ from abc import ABCMeta, abstractmethod import six import openpype -from openpype.settings import get_system_settings -from openpype.settings.lib import get_studio_system_settings_overrides +from openpype.settings import ( + get_system_settings, + SYSTEM_SETTINGS_KEY, + PROJECT_SETTINGS_KEY, + SCHEMA_KEY_SYSTEM_SETTINGS, + SCHEMA_KEY_PROJECT_SETTINGS +) + +from openpype.settings.lib import ( + get_studio_system_settings_overrides, +) from openpype.lib import PypeLogger @@ -1030,7 +1039,7 @@ def get_module_settings_defs(): @six.add_metaclass(ABCMeta) -class ModuleSettingsDef: +class BaseModuleSettingsDef: """Definition of settings for OpenPype module or AddOn.""" _id = None @@ -1091,3 +1100,112 @@ class ModuleSettingsDef: Passed data are by path to first key defined in main schemas. """ pass + + +class ModuleSettingsDef(BaseModuleSettingsDef): + def get_defaults(self, top_key): + """Split method into 2 methods by top key.""" + if top_key == SYSTEM_SETTINGS_KEY: + return self.get_default_system_settings() or {} + elif top_key == PROJECT_SETTINGS_KEY: + return self.get_default_project_settings() or {} + return {} + + def save_defaults(self, top_key, data): + """Split method into 2 methods by top key.""" + if top_key == SYSTEM_SETTINGS_KEY: + self.save_system_defaults(data) + elif top_key == PROJECT_SETTINGS_KEY: + self.save_project_defaults(data) + + def get_settings_schemas(self, schema_type): + """Split method into 2 methods by schema type.""" + if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS: + return self.get_system_settings_schemas() or {} + elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS: + return self.get_project_settings_schemas() or {} + return {} + + def get_dynamic_schemas(self, schema_type): + """Split method into 2 methods by schema type.""" + if schema_type == SCHEMA_KEY_SYSTEM_SETTINGS: + return self.get_system_dynamic_schemas() or {} + elif schema_type == SCHEMA_KEY_PROJECT_SETTINGS: + return self.get_project_dynamic_schemas() or {} + return {} + + @abstractmethod + def get_system_settings_schemas(self): + """Schemas and templates usable in system settings schemas. + + Returns: + dict: Schemas and templates by it's names. Names must be unique + across whole OpenPype. + """ + pass + + @abstractmethod + def get_project_settings_schemas(self): + """Schemas and templates usable in project settings schemas. + + Returns: + dict: Schemas and templates by it's names. Names must be unique + across whole OpenPype. + """ + pass + + @abstractmethod + def get_system_dynamic_schemas(self): + """System schemas by dynamic schema name. + + If dynamic schema name is not available in then schema will not used. + + Returns: + dict: Schemas or list of schemas by dynamic schema name. + """ + pass + + @abstractmethod + def get_project_dynamic_schemas(self): + """Project schemas by dynamic schema name. + + If dynamic schema name is not available in then schema will not used. + + Returns: + dict: Schemas or list of schemas by dynamic schema name. + """ + pass + + @abstractmethod + def get_default_system_settings(self): + """Default system settings values. + + Returns: + dict: Default values by path to first key. + """ + pass + + @abstractmethod + def get_default_project_settings(self): + """Default project settings values. + + Returns: + dict: Default values by path to first key. + """ + pass + + @abstractmethod + def save_system_defaults(self, data): + """Save default system settings values. + + Passed data are by path to first key defined in main schemas. + """ + pass + + @abstractmethod + def save_project_defaults(self, data): + """Save default project settings values. + + Passed data are by path to first key defined in main schemas. + """ + pass From ab7ea51bab5eacfb8ccd6d4c913acb89b115855e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 17:12:35 +0200 Subject: [PATCH 046/115] preimplemented json files settings definition which needs only one method to implement --- openpype/modules/__init__.py | 4 + openpype/modules/base.py | 195 +++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index 6b3c0dc3a6..68b5f6c247 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -11,6 +11,8 @@ from .base import ( BaseModuleSettingsDef, ModuleSettingsDef, + JsonFilesSettingsDef, + get_module_settings_defs ) @@ -27,5 +29,7 @@ __all__ = ( "BaseModuleSettingsDef", "ModuleSettingsDef", + "JsonFilesSettingsDef", + "get_module_settings_defs" ) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 66f962526f..a11867ea15 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -2,6 +2,7 @@ """Base class for Pype Modules.""" import os import sys +import json import time import inspect import logging @@ -23,6 +24,7 @@ from openpype.settings import ( from openpype.settings.lib import ( get_studio_system_settings_overrides, + load_json_file ) from openpype.lib import PypeLogger @@ -1103,6 +1105,11 @@ class BaseModuleSettingsDef: class ModuleSettingsDef(BaseModuleSettingsDef): + """Settings definiton with separated system and procect settings parts. + + Reduce conditions that must be checked and adds predefined methods for + each case. + """ def get_defaults(self, top_key): """Split method into 2 methods by top key.""" if top_key == SYSTEM_SETTINGS_KEY: @@ -1209,3 +1216,191 @@ class ModuleSettingsDef(BaseModuleSettingsDef): Passed data are by path to first key defined in main schemas. """ pass + + +class JsonFilesSettingsDef(ModuleSettingsDef): + """Preimplemented settings definition using json files and file structure. + + Expected file structure: + ┕ root + │ + │ # Default values + ┝ defaults + │ ┝ system_settings.json + │ ┕ project_settings.json + │ + │ # Schemas for `dynamic_template` type + ┝ dynamic_schemas + │ ┝ system_dynamic_schemas.json + │ ┕ project_dynamic_schemas.json + │ + │ # Schemas that can be used anywhere (enhancement for `dynamic_schemas`) + ┕ schemas + ┝ system_schemas + │ ┝ # Any schema or template files + │ ┕ ... + ┕ project_schemas + ┝ # Any schema or template files + ┕ ... + + Schemas can be loaded with prefix to avoid duplicated schema/template names + across all OpenPype addons/modules. Prefix can be defined with class + attribute `schema_prefix`. + + Only think which must be implemented in `get_settings_root_dir` which + should return directory path to `root` (in structure graph above). + """ + # Possible way how to define `schemas` prefix + schema_prefix = "" + + @abstractmethod + def get_settings_root_dir(self): + """Directory path where settings and it's schemas are located.""" + pass + + def __init__(self): + settings_root_dir = self.get_settings_root_dir() + defaults_dir = os.path.join( + settings_root_dir, "defaults" + ) + dynamic_schemas_dir = os.path.join( + settings_root_dir, "dynamic_schemas" + ) + schemas_dir = os.path.join( + settings_root_dir, "schemas" + ) + + self.system_defaults_filepath = os.path.join( + defaults_dir, "system_settings.json" + ) + self.project_defaults_filepath = os.path.join( + defaults_dir, "project_settings.json" + ) + + self.system_dynamic_schemas_filepath = os.path.join( + dynamic_schemas_dir, "system_dynamic_schemas.json" + ) + self.project_dynamic_schemas_filepath = os.path.join( + dynamic_schemas_dir, "project_dynamic_schemas.json" + ) + + self.system_schemas_dir = os.path.join( + schemas_dir, "system_schemas" + ) + self.project_schemas_dir = os.path.join( + schemas_dir, "project_schemas" + ) + + def _load_json_file_data(self, path): + if os.path.exists(path): + return load_json_file(path) + return {} + + def get_default_system_settings(self): + """Default system settings values. + + Returns: + dict: Default values by path to first key. + """ + return self._load_json_file_data(self.system_defaults_filepath) + + def get_default_project_settings(self): + """Default project settings values. + + Returns: + dict: Default values by path to first key. + """ + return self._load_json_file_data(self.project_defaults_filepath) + + def _save_data_to_filepath(self, path, data): + dirpath = os.path.dirname(path) + if not os.path.exists(dirpath): + os.makedirs(dirpath) + + with open(path, "w") as file_stream: + json.dump(data, file_stream, indent=4) + + def save_system_defaults(self, data): + """Save default system settings values. + + Passed data are by path to first key defined in main schemas. + """ + self._save_data_to_filepath(self.system_defaults_filepath, data) + + def save_project_defaults(self, data): + """Save default project settings values. + + Passed data are by path to first key defined in main schemas. + """ + self._save_data_to_filepath(self.project_defaults_filepath, data) + + def get_system_dynamic_schemas(self): + """System schemas by dynamic schema name. + + If dynamic schema name is not available in then schema will not used. + + Returns: + dict: Schemas or list of schemas by dynamic schema name. + """ + return self._load_json_file_data(self.system_dynamic_schemas_filepath) + + def get_project_dynamic_schemas(self): + """Project schemas by dynamic schema name. + + If dynamic schema name is not available in then schema will not used. + + Returns: + dict: Schemas or list of schemas by dynamic schema name. + """ + return self._load_json_file_data(self.project_dynamic_schemas_filepath) + + def _load_files_from_path(self, path): + output = {} + if not path or not os.path.exists(path): + return output + + if os.path.isfile(path): + filename = os.path.basename(path) + basename, ext = os.path.splitext(filename) + if ext == ".json": + if self.schema_prefix: + key = "{}/{}".format(self.schema_prefix, basename) + else: + key = basename + output[key] = self._load_json_file_data(path) + return output + + path = os.path.normpath(path) + for root, _, files in os.walk(path, topdown=False): + for filename in files: + basename, ext = os.path.splitext(filename) + if ext != ".json": + continue + + json_path = os.path.join(root, filename) + store_key = os.path.join( + root.replace(path, ""), basename + ).replace("\\", "/") + if self.schema_prefix: + store_key = "{}/{}".format(self.schema_prefix, store_key) + output[store_key] = self._load_json_file_data(json_path) + + return output + + def get_system_settings_schemas(self): + """Schemas and templates usable in system settings schemas. + + Returns: + dict: Schemas and templates by it's names. Names must be unique + across whole OpenPype. + """ + return self._load_files_from_path(self.system_schemas_dir) + + def get_project_settings_schemas(self): + """Schemas and templates usable in project settings schemas. + + Returns: + dict: Schemas and templates by it's names. Names must be unique + across whole OpenPype. + """ + return self._load_files_from_path(self.project_schemas_dir) From 82f361d48aa041e6b9da4b2c919dc567e2f8aae1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 17:14:20 +0200 Subject: [PATCH 047/115] enable addon by default --- openpype/modules/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index a11867ea15..1fc1d900a0 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -431,7 +431,8 @@ class OpenPypeModule: class OpenPypeAddOn(OpenPypeModule): - pass + # Enable Addon by default + enabled = True class ModulesManager: From 2188e62b694dc53a7a07959ee51ebbf36ef1f3d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 17:14:33 +0200 Subject: [PATCH 048/115] implement abstract methods required for modules --- openpype/modules/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1fc1d900a0..23ec3b8c6f 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -434,6 +434,14 @@ class OpenPypeAddOn(OpenPypeModule): # Enable Addon by default enabled = True + def initialize(self, module_settings): + """Initialization is not be required for most of addons.""" + pass + + def connect_with_modules(self, enabled_modules): + """Do not require to implement connection with modules for addon.""" + pass + class ModulesManager: """Manager of Pype modules helps to load and prepare them to work. From 5dd6d010609f91647b416d81e38ac282f7de3269 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 27 Aug 2021 17:23:34 +0200 Subject: [PATCH 049/115] changed name of `get_settings_root_dir` to `get_settings_root_path` --- openpype/modules/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 23ec3b8c6f..01c3cebe60 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -1256,19 +1256,19 @@ class JsonFilesSettingsDef(ModuleSettingsDef): across all OpenPype addons/modules. Prefix can be defined with class attribute `schema_prefix`. - Only think which must be implemented in `get_settings_root_dir` which + Only think which must be implemented in `get_settings_root_path` which should return directory path to `root` (in structure graph above). """ # Possible way how to define `schemas` prefix schema_prefix = "" @abstractmethod - def get_settings_root_dir(self): + def get_settings_root_path(self): """Directory path where settings and it's schemas are located.""" pass def __init__(self): - settings_root_dir = self.get_settings_root_dir() + settings_root_dir = self.get_settings_root_path() defaults_dir = os.path.join( settings_root_dir, "defaults" ) From dd4c4342e0a36dff7ef7366ebf13849a79074aee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Aug 2021 11:33:00 +0200 Subject: [PATCH 050/115] recalculate relativelly offset by value --- openpype/widgets/sliders.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/widgets/sliders.py b/openpype/widgets/sliders.py index 2f26c3eb97..32ade58af5 100644 --- a/openpype/widgets/sliders.py +++ b/openpype/widgets/sliders.py @@ -70,18 +70,19 @@ class NiceSlider(QtWidgets.QSlider): ) _range = self.maximum() - self.minimum() + _offset = self.value() - self.minimum() if horizontal: _handle_half = rect.height() / 2 _handle_size = _handle_half * 2 width = rect.width() - _handle_size - pos_x = ((width / _range) * self.value()) + pos_x = ((width / _range) * _offset) pos_y = rect.center().y() - _handle_half + 1 else: _handle_half = rect.width() / 2 _handle_size = _handle_half * 2 height = rect.height() - _handle_size pos_x = rect.center().x() - _handle_half + 1 - pos_y = height - ((height / _range) * self.value()) + pos_y = height - ((height / _range) * _offset) handle_rect = QtCore.QRect( pos_x, pos_y, _handle_size, _handle_size From d6fc47d1c1ae6914b138f65878052dc4af874705 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Aug 2021 11:34:51 +0200 Subject: [PATCH 051/115] added option to have sliders in number widgets --- openpype/settings/entities/input_entities.py | 3 ++ .../tools/settings/settings/item_widgets.py | 41 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 336d1f5c1e..f7e85294a2 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -369,6 +369,9 @@ class NumberEntity(InputEntity): self.valid_value_types = valid_value_types self.value_on_not_set = value_on_not_set + # UI specific attributes + self.show_slider = self.schema_data.get("show_slider", False) + def _convert_to_valid_type(self, value): if isinstance(value, str): new_value = None diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index d29fa6f42b..6f304a1f88 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -21,6 +21,7 @@ from .base import ( BaseWidget, InputWidget ) +from openpype.widgets.sliders import NiceSlider from openpype.tools.settings import CHILD_OFFSET @@ -377,6 +378,8 @@ class TextWidget(InputWidget): class NumberWidget(InputWidget): + _slider_widget = None + def _add_inputs_to_layout(self): kwargs = { "minimum": self.entity.minimum, @@ -384,14 +387,33 @@ class NumberWidget(InputWidget): "decimal": self.entity.decimal } self.input_field = NumberSpinBox(self.content_widget, **kwargs) + input_field_stretch = 1 + + if self.entity.show_slider: + slider_widget = NiceSlider(QtCore.Qt.Horizontal, self) + slider_widget.setRange( + self.entity.minimum, + self.entity.maximum + ) + + self.content_layout.addWidget(slider_widget, 1) + + slider_widget.valueChanged.connect(self._on_slider_change) + + self._slider_widget = slider_widget + + input_field_stretch = 0 self.setFocusProxy(self.input_field) - self.content_layout.addWidget(self.input_field, 1) + self.content_layout.addWidget(self.input_field, input_field_stretch) self.input_field.valueChanged.connect(self._on_value_change) self.input_field.focused_in.connect(self._on_input_focus) + self._ignore_slider_change = False + self._ignore_input_change = False + def _on_input_focus(self): self.focused_in() @@ -402,10 +424,25 @@ class NumberWidget(InputWidget): def set_entity_value(self): self.input_field.setValue(self.entity.value) + def _on_slider_change(self, new_value): + if self._ignore_slider_change: + return + + self._ignore_input_change = True + self.input_field.setValue(new_value) + self._ignore_input_change = False + def _on_value_change(self): if self.ignore_input_changes: return - self.entity.set(self.input_field.value()) + + value = self.input_field.value() + if self._slider_widget is not None and not self._ignore_input_change: + self._ignore_slider_change = True + self._slider_widget.setValue(value) + self._ignore_slider_change = False + + self.entity.set(value) class RawJsonInput(SettingsPlainTextEdit): From 25478317d619909422a0f16a5b5e792b52ec24ab Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 10:42:47 +0200 Subject: [PATCH 052/115] modified sizes of slider in style --- .../tools/settings/settings/style/style.css | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/tools/settings/settings/style/style.css b/openpype/tools/settings/settings/style/style.css index 250c15063f..d9d85a481e 100644 --- a/openpype/tools/settings/settings/style/style.css +++ b/openpype/tools/settings/settings/style/style.css @@ -114,6 +114,30 @@ QPushButton[btn-type="expand-toggle"] { background: #21252B; } +/* SLider */ +QSlider::groove { + border: 1px solid #464b54; + border-radius: 0.3em; +} +QSlider::groove:horizontal { + height: 8px; +} +QSlider::groove:vertical { + width: 8px; +} +QSlider::handle { + width: 10px; + height: 10px; + + border-radius: 5px; +} +QSlider::handle:horizontal { + margin: -2px 0; +} +QSlider::handle:vertical { + margin: 0 -2px; +} + #GroupWidget { border-bottom: 1px solid #21252B; } From ff4ed83519c89473925999b7d749b31405187365 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 10:58:50 +0200 Subject: [PATCH 053/115] added slider multiplier as slider can't handle decimal places --- openpype/tools/settings/settings/item_widgets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 6f304a1f88..1f74308211 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -389,11 +389,13 @@ class NumberWidget(InputWidget): self.input_field = NumberSpinBox(self.content_widget, **kwargs) input_field_stretch = 1 + self._slider_multiplier = 10 ** self.entity.decimal if self.entity.show_slider: + slider_widget = NiceSlider(QtCore.Qt.Horizontal, self) slider_widget.setRange( - self.entity.minimum, - self.entity.maximum + int(self.entity.minimum * self._slider_multiplier), + int(self.entity.maximum * self._slider_multiplier) ) self.content_layout.addWidget(slider_widget, 1) @@ -429,7 +431,7 @@ class NumberWidget(InputWidget): return self._ignore_input_change = True - self.input_field.setValue(new_value) + self.input_field.setValue(new_value / self._slider_multiplier) self._ignore_input_change = False def _on_value_change(self): From 00d1ae5d43500116f735522e5db79b8a92ca777b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 10:58:59 +0200 Subject: [PATCH 054/115] added show_slider to examples --- .../entities/schemas/system_schema/example_schema.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index f633d5cb1a..af6a2d49f4 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -183,6 +183,15 @@ "minimum": -10, "maximum": -5 }, + { + "type": "number", + "key": "number_with_slider", + "label": "Number with slider", + "decimal": 2, + "minimum": 0.0, + "maximum": 1.0, + "show_slider": true + }, { "type": "text", "key": "singleline_text", From 8f5254ff234e7bd88298bcb230b1cd44252f1d7c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 11:00:44 +0200 Subject: [PATCH 055/115] added show slider to readme --- openpype/settings/entities/schemas/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 2034d4e463..2709f5bed9 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -300,6 +300,7 @@ How output of the schema could look like on save: - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +- for UI it is possible to show slider to enable this option set `show_slider` to `true` ``` { "type": "number", @@ -311,6 +312,18 @@ How output of the schema could look like on save: } ``` +``` +{ + "type": "number", + "key": "ratio", + "label": "Ratio" + "decimal": 3, + "minimum": 0, + "maximum": 1, + "show_slider": true +} +``` + ### text - simple text input - key `"multiline"` allows to enter multiple lines of text (Default: `False`) From b8608bccaeaba9810473d6516124ea5d14b04cc6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 31 Aug 2021 11:10:49 +0200 Subject: [PATCH 056/115] added explaining comment to slider multiplier --- openpype/tools/settings/settings/item_widgets.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 1f74308211..a7b1208269 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -389,13 +389,15 @@ class NumberWidget(InputWidget): self.input_field = NumberSpinBox(self.content_widget, **kwargs) input_field_stretch = 1 - self._slider_multiplier = 10 ** self.entity.decimal + slider_multiplier = 1 if self.entity.show_slider: - + # Slider can't handle float numbers so all decimals are converted + # to integer range. + slider_multiplier = 10 ** self.entity.decimal slider_widget = NiceSlider(QtCore.Qt.Horizontal, self) slider_widget.setRange( - int(self.entity.minimum * self._slider_multiplier), - int(self.entity.maximum * self._slider_multiplier) + int(self.entity.minimum * slider_multiplier), + int(self.entity.maximum * slider_multiplier) ) self.content_layout.addWidget(slider_widget, 1) @@ -406,6 +408,8 @@ class NumberWidget(InputWidget): input_field_stretch = 0 + self._slider_multiplier = slider_multiplier + self.setFocusProxy(self.input_field) self.content_layout.addWidget(self.input_field, input_field_stretch) From 901e5f52666f36f7accbc95ace2d9abbc4f6c993 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 31 Aug 2021 18:24:24 +0200 Subject: [PATCH 057/115] refactor common code, change handling of env var --- igniter/install_dialog.py | 33 +++--------------------------- igniter/nice_progress_bar.py | 20 ++++++++++++++++++ igniter/tools.py | 12 +++++++++++ igniter/update_window.py | 39 ++---------------------------------- start.py | 12 +++++++---- 5 files changed, 45 insertions(+), 71 deletions(-) create mode 100644 igniter/nice_progress_bar.py diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index 1ec8cc6768..1fe67e3397 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -14,21 +14,13 @@ from .tools import ( validate_mongo_connection, get_openpype_path_from_db ) + +from .nice_progress_bar import NiceProgressBar from .user_settings import OpenPypeSecureRegistry +from .tools import load_stylesheet from .version import __version__ -def load_stylesheet(): - stylesheet_path = os.path.join( - os.path.dirname(__file__), - "stylesheet.css" - ) - with open(stylesheet_path, "r") as file_stream: - stylesheet = file_stream.read() - - return stylesheet - - class ButtonWithOptions(QtWidgets.QFrame): option_clicked = QtCore.Signal(str) @@ -91,25 +83,6 @@ class ButtonWithOptions(QtWidgets.QFrame): self.option_clicked.emit(self._default_value) -class NiceProgressBar(QtWidgets.QProgressBar): - def __init__(self, parent=None): - super(NiceProgressBar, self).__init__(parent) - self._real_value = 0 - - def setValue(self, value): - self._real_value = value - if value != 0 and value < 11: - value = 11 - - super(NiceProgressBar, self).setValue(value) - - def value(self): - return self._real_value - - def text(self): - return "{} %".format(self._real_value) - - class ConsoleWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(ConsoleWidget, self).__init__(parent) diff --git a/igniter/nice_progress_bar.py b/igniter/nice_progress_bar.py new file mode 100644 index 0000000000..47d695a101 --- /dev/null +++ b/igniter/nice_progress_bar.py @@ -0,0 +1,20 @@ +from Qt import QtCore, QtGui, QtWidgets # noqa + + +class NiceProgressBar(QtWidgets.QProgressBar): + def __init__(self, parent=None): + super(NiceProgressBar, self).__init__(parent) + self._real_value = 0 + + def setValue(self, value): + self._real_value = value + if value != 0 and value < 11: + value = 11 + + super(NiceProgressBar, self).setValue(value) + + def value(self): + return self._real_value + + def text(self): + return "{} %".format(self._real_value) diff --git a/igniter/tools.py b/igniter/tools.py index 529d535c25..c0fa97d03e 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -248,3 +248,15 @@ def get_openpype_path_from_db(url: str) -> Union[str, None]: if os.path.exists(path): return path return None + + +def load_stylesheet() -> str: + """Load css style sheet. + + Returns: + str: content of the stylesheet + + """ + stylesheet_path = Path(__file__).parent.resolve() / "stylesheet.css" + + return stylesheet_path.read_text() \ No newline at end of file diff --git a/igniter/update_window.py b/igniter/update_window.py index a49a84cfee..e443201e09 100644 --- a/igniter/update_window.py +++ b/igniter/update_window.py @@ -5,43 +5,8 @@ from pathlib import Path from .update_thread import UpdateThread from Qt import QtCore, QtGui, QtWidgets # noqa from .bootstrap_repos import OpenPypeVersion - - -def load_stylesheet(path: str = None) -> str: - """Load css style sheet. - - Args: - path (str, optional): Path to stylesheet. If none, `stylesheet.css` - from current package's path is used. - Returns: - str: content of the stylesheet - - """ - if path: - stylesheet_path = Path(path) - else: - stylesheet_path = Path(os.path.dirname(__file__)) / "stylesheet.css" - - return stylesheet_path.read_text() - - -class NiceProgressBar(QtWidgets.QProgressBar): - def __init__(self, parent=None): - super(NiceProgressBar, self).__init__(parent) - self._real_value = 0 - - def setValue(self, value): - self._real_value = value - if value != 0 and value < 11: - value = 11 - - super(NiceProgressBar, self).setValue(value) - - def value(self): - return self._real_value - - def text(self): - return "{} %".format(self._real_value) +from .nice_progress_bar import NiceProgressBar +from .tools import load_stylesheet class UpdateWindow(QtWidgets.QDialog): diff --git a/start.py b/start.py index 9e60d79f04..2e45dc4df3 100644 --- a/start.py +++ b/start.py @@ -181,6 +181,10 @@ else: if "--headless" in sys.argv: os.environ["OPENPYPE_HEADLESS_MODE"] = "1" + sys.argv.remove("--headless") +else: + if os.getenv("OPENPYPE_HEADLESS_MODE") != "1": + os.environ.pop("OPENPYPE_HEADLESS_MODE") import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 @@ -397,7 +401,7 @@ def _process_arguments() -> tuple: # handle igniter # this is helper to run igniter before anything else if "igniter" in sys.argv: - if os.getenv("OPENPYPE_HEADLESS_MODE"): + if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": _print("!!! Cannot open Igniter dialog in headless mode.") sys.exit(1) import igniter @@ -447,7 +451,7 @@ def _determine_mongodb() -> str: if not openpype_mongo: _print("*** No DB connection string specified.") - if os.getenv("OPENPYPE_HEADLESS_MODE"): + if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": _print("!!! Cannot open Igniter dialog in headless mode.") _print( "!!! Please use `OPENPYPE_MONGO` to specify server address.") @@ -555,7 +559,7 @@ def _find_frozen_openpype(use_version: str = None, except IndexError: # no OpenPype version found, run Igniter and ask for them. _print('*** No OpenPype versions found.') - if os.getenv("OPENPYPE_HEADLESS_MODE"): + if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": _print("!!! Cannot open Igniter dialog in headless mode.") sys.exit(1) _print("--- launching setup UI ...") @@ -621,7 +625,7 @@ def _find_frozen_openpype(use_version: str = None, if not is_inside: # install latest version to user data dir - if not os.getenv("OPENPYPE_HEADLESS_MODE"): + if os.getenv("OPENPYPE_HEADLESS_MODE", "0") != "1": import igniter version_path = igniter.open_update_window(openpype_version) else: From b59bb52b6b7abc55e38132cf3175b86489b995cd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 31 Aug 2021 18:27:26 +0200 Subject: [PATCH 058/115] =?UTF-8?q?hound=20fixes=20=F0=9F=90=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- igniter/tools.py | 2 +- igniter/update_window.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/igniter/tools.py b/igniter/tools.py index c0fa97d03e..c934289064 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -259,4 +259,4 @@ def load_stylesheet() -> str: """ stylesheet_path = Path(__file__).parent.resolve() / "stylesheet.css" - return stylesheet_path.read_text() \ No newline at end of file + return stylesheet_path.read_text() diff --git a/igniter/update_window.py b/igniter/update_window.py index e443201e09..d7908c240b 100644 --- a/igniter/update_window.py +++ b/igniter/update_window.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- """Progress window to show when OpenPype is updating/installing locally.""" import os -from pathlib import Path from .update_thread import UpdateThread from Qt import QtCore, QtGui, QtWidgets # noqa from .bootstrap_repos import OpenPypeVersion From 537b9e7bab559419c37460c1c67d709bb482169a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 08:31:18 +0100 Subject: [PATCH 059/115] Stop timer was within validator order range. --- openpype/plugins/publish/stop_timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/stop_timer.py b/openpype/plugins/publish/stop_timer.py index 81afd16378..5c939b5f1b 100644 --- a/openpype/plugins/publish/stop_timer.py +++ b/openpype/plugins/publish/stop_timer.py @@ -8,7 +8,7 @@ from openpype.api import get_system_settings class StopTimer(pyblish.api.ContextPlugin): label = "Stop Timer" - order = pyblish.api.ExtractorOrder - 0.5 + order = pyblish.api.ExtractorOrder - 0.49 hosts = ["*"] def process(self, context): From e3f0e89e2129eaa99eba436ed5d2fe43402ce77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Wed, 1 Sep 2021 13:05:43 +0200 Subject: [PATCH 060/115] default value for pop Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 2e45dc4df3..00f9a50cbb 100644 --- a/start.py +++ b/start.py @@ -184,7 +184,7 @@ if "--headless" in sys.argv: sys.argv.remove("--headless") else: if os.getenv("OPENPYPE_HEADLESS_MODE") != "1": - os.environ.pop("OPENPYPE_HEADLESS_MODE") + os.environ.pop("OPENPYPE_HEADLESS_MODE", None) import igniter # noqa: E402 from igniter import BootstrapRepos # noqa: E402 From 84ee712ef4b9efe22c80baa86842fa1f739f07d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Sep 2021 16:32:41 +0200 Subject: [PATCH 061/115] change collect host name order to lower possible --- openpype/plugins/publish/collect_host_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_host_name.py b/openpype/plugins/publish/collect_host_name.py index 41d9cc3a5a..b731e3ed26 100644 --- a/openpype/plugins/publish/collect_host_name.py +++ b/openpype/plugins/publish/collect_host_name.py @@ -14,7 +14,7 @@ class CollectHostName(pyblish.api.ContextPlugin): """Collect avalon host name to context.""" label = "Collect Host Name" - order = pyblish.api.CollectorOrder - 1 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): host_name = context.data.get("hostName") From 69b8659b6d99cf2a547418467577c9bf9e2e1172 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Sep 2021 16:33:58 +0200 Subject: [PATCH 062/115] change tvpaint collectors order --- openpype/hosts/tvpaint/plugins/publish/collect_instances.py | 2 +- openpype/hosts/tvpaint/plugins/publish/collect_workfile.py | 2 +- openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index e496b144cd..dfa8f17ee9 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -9,7 +9,7 @@ from openpype.lib import get_subset_name class CollectInstances(pyblish.api.ContextPlugin): label = "Collect Instances" - order = pyblish.api.CollectorOrder - 1 + order = pyblish.api.CollectorOrder - 0.4 hosts = ["tvpaint"] def process(self, context): diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py index b61fec895f..65e38ea258 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -8,7 +8,7 @@ from openpype.lib import get_subset_name class CollectWorkfile(pyblish.api.ContextPlugin): label = "Collect Workfile" - order = pyblish.api.CollectorOrder - 1 + order = pyblish.api.CollectorOrder - 0.4 hosts = ["tvpaint"] def process(self, context): diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 79cc01740a..e87c08fda8 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -39,7 +39,7 @@ class ResetTVPaintWorkfileMetadata(pyblish.api.Action): class CollectWorkfileData(pyblish.api.ContextPlugin): label = "Collect Workfile Data" - order = pyblish.api.CollectorOrder - 1.01 + order = pyblish.api.CollectorOrder - 0.5 hosts = ["tvpaint"] actions = [ResetTVPaintWorkfileMetadata] From 1f6a3fdf4286aa3c4291225a02d9eb7af9912088 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 1 Sep 2021 16:43:27 +0200 Subject: [PATCH 063/115] moved CollectWorkfileData in tvpaint --- openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index e87c08fda8..f4259f1b5f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -39,7 +39,7 @@ class ResetTVPaintWorkfileMetadata(pyblish.api.Action): class CollectWorkfileData(pyblish.api.ContextPlugin): label = "Collect Workfile Data" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.45 hosts = ["tvpaint"] actions = [ResetTVPaintWorkfileMetadata] From 4ec7e18aad0044164fd56688dc70183205401866 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:25:19 +0200 Subject: [PATCH 064/115] pass workfile template to `_prepare_last_workfile` --- openpype/lib/applications.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index fbf991a32e..45b8e6468d 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -29,7 +29,7 @@ from .local_settings import get_openpype_username from .avalon_context import ( get_workdir_data, get_workdir_with_workdir_data, - get_workfile_template_key_from_context + get_workfile_template_key ) from .python_module_tools import ( @@ -1226,8 +1226,12 @@ def prepare_context_environments(data): # Load project specific environments project_name = project_doc["name"] + project_settings = get_project_settings(project_name) + data["project_settings"] = project_settings # Apply project specific environments on current env value - apply_project_environments_value(project_name, data["env"]) + apply_project_environments_value( + project_name, data["env"], project_settings + ) app = data["app"] workdir_data = get_workdir_data( @@ -1237,17 +1241,19 @@ def prepare_context_environments(data): anatomy = data["anatomy"] - template_key = get_workfile_template_key_from_context( - asset_doc["name"], - task_name, + asset_tasks = asset_doc.get("data", {}).get("tasks") or {} + task_info = asset_tasks.get(task_name) or {} + task_type = task_info.get("type") + workfile_template_key = get_workfile_template_key( + task_type, app.host_name, project_name=project_name, - dbcon=data["dbcon"] + project_settings=project_settings ) try: workdir = get_workdir_with_workdir_data( - workdir_data, anatomy, template_key=template_key + workdir_data, anatomy, template_key=workfile_template_key ) except Exception as exc: @@ -1281,10 +1287,10 @@ def prepare_context_environments(data): ) data["env"].update(context_env) - _prepare_last_workfile(data, workdir) + _prepare_last_workfile(data, workdir, workfile_template_key) -def _prepare_last_workfile(data, workdir): +def _prepare_last_workfile(data, workdir, workfile_template_key): """last workfile workflow preparation. Function check if should care about last workfile workflow and tries @@ -1345,7 +1351,7 @@ def _prepare_last_workfile(data, workdir): if extensions: anatomy = data["anatomy"] # Find last workfile - file_template = anatomy.templates["work"]["file"] + file_template = anatomy.templates[workfile_template_key]["file"] workdir_data.update({ "version": 1, "user": get_openpype_username(), From 23f17609db0549f1b8ed56f10e530b1330adb109 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:25:33 +0200 Subject: [PATCH 065/115] removed todo which is already done --- openpype/tools/workfiles/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index b542e6e718..3d2633f8dc 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -430,7 +430,6 @@ class FilesWidget(QtWidgets.QWidget): # Pype's anatomy object for current project self.anatomy = Anatomy(io.Session["AVALON_PROJECT"]) # Template key used to get work template from anatomy templates - # TODO change template key based on task self.template_key = "work" # This is not root but workfile directory From e43f7bc007a74f033f39ddc4dee628e183518218 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:34:18 +0200 Subject: [PATCH 066/115] removed duplicated line --- .../standalonepublisher/plugins/publish/extract_harmony_zip.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py index f7f96c7d03..e3e5e94d30 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py @@ -244,7 +244,6 @@ class ExtractHarmonyZip(openpype.api.Extractor): os.path.dirname(work_path), file_template, data, [".zip"] )[1] - work_path = anatomy_filled["work"]["path"] base_name = os.path.splitext(os.path.basename(work_path))[0] staging_work_path = os.path.join(os.path.dirname(staging_scene), From 2f9f1ad00c72c52a17bbdd01d46672c03e334a64 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:34:42 +0200 Subject: [PATCH 067/115] use `HOST_WORKFILE_EXTENSIONS` to get workfile extensions --- .../plugins/publish/extract_harmony_zip.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py index e3e5e94d30..e422837441 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py @@ -233,6 +233,7 @@ class ExtractHarmonyZip(openpype.api.Extractor): "version": 1, "ext": "zip", } + host_name = "harmony" # Get a valid work filename first with version 1 file_template = anatomy.templates["work"]["file"] @@ -241,7 +242,10 @@ class ExtractHarmonyZip(openpype.api.Extractor): # Get the final work filename with the proper version data["version"] = api.last_workfile_with_version( - os.path.dirname(work_path), file_template, data, [".zip"] + os.path.dirname(work_path), + file_template, + data, + api.HOST_WORKFILE_EXTENSIONS[host_name] )[1] base_name = os.path.splitext(os.path.basename(work_path))[0] From 79819a21a01f17d907d116789932976b7d9f9321 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:34:54 +0200 Subject: [PATCH 068/115] use get_workfile_template_key_from_context to get right work template name --- .../plugins/publish/extract_harmony_zip.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py index e422837441..85da01c890 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py @@ -11,6 +11,7 @@ import zipfile import pyblish.api from avalon import api, io import openpype.api +from openpype.lib import get_workfile_template_key_from_context class ExtractHarmonyZip(openpype.api.Extractor): @@ -234,11 +235,18 @@ class ExtractHarmonyZip(openpype.api.Extractor): "ext": "zip", } host_name = "harmony" + template_name = get_workfile_template_key_from_context( + instance.data["asset"], + instance.data.get("task"), + host_name, + project_name=project_entity["name"], + dbcon=io + ) # Get a valid work filename first with version 1 - file_template = anatomy.templates["work"]["file"] + file_template = anatomy.templates[template_name]["file"] anatomy_filled = anatomy.format(data) - work_path = anatomy_filled["work"]["path"] + work_path = anatomy_filled[template_name]["path"] # Get the final work filename with the proper version data["version"] = api.last_workfile_with_version( From e9c2275d046c0b02e82493bddde5df4fb74e1200 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 2 Sep 2021 14:28:03 +0200 Subject: [PATCH 069/115] remove ftrack submodules --- .gitmodules | 2 +- openpype/modules/ftrack/python2_vendor/arrow | 1 - openpype/modules/ftrack/python2_vendor/ftrack-python-api | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 160000 openpype/modules/ftrack/python2_vendor/arrow delete mode 160000 openpype/modules/ftrack/python2_vendor/ftrack-python-api diff --git a/.gitmodules b/.gitmodules index 28f164726d..e1b0917e9d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,4 +9,4 @@ url = https://github.com/arrow-py/arrow.git [submodule "openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api"] path = openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api - url = https://bitbucket.org/ftrack/ftrack-python-api.git + url = https://bitbucket.org/ftrack/ftrack-python-api.git \ No newline at end of file diff --git a/openpype/modules/ftrack/python2_vendor/arrow b/openpype/modules/ftrack/python2_vendor/arrow deleted file mode 160000 index b746fedf72..0000000000 --- a/openpype/modules/ftrack/python2_vendor/arrow +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b746fedf7286c3755a46f07ab72f4c414cd41fc0 diff --git a/openpype/modules/ftrack/python2_vendor/ftrack-python-api b/openpype/modules/ftrack/python2_vendor/ftrack-python-api deleted file mode 160000 index d277f474ab..0000000000 --- a/openpype/modules/ftrack/python2_vendor/ftrack-python-api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d277f474ab016e7b53479c36af87cb861d0cc53e From b6754d8827a1761d2a70792713bf4317a5c6a25d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:46:15 +0200 Subject: [PATCH 070/115] added single selection option to task type enum --- openpype/settings/entities/enum_entity.py | 57 +++++++++++++++++++---- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index cb532c5ae0..6c0e63fa1f 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -376,11 +376,16 @@ class TaskTypeEnumEntity(BaseEnumEntity): schema_types = ["task-types-enum"] def _item_initalization(self): - self.multiselection = True - self.value_on_not_set = [] + self.multiselection = self.schema_data.get("multiselection", True) + if self.multiselection: + self.valid_value_types = (list, ) + self.value_on_not_set = [] + else: + self.valid_value_types = (STRING_TYPE, ) + 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): @@ -396,15 +401,51 @@ class TaskTypeEnumEntity(BaseEnumEntity): return enum_items, valid_keys + def _convert_value_for_current_state(self, source_value): + if self.multiselection: + output = [] + for key in source_value: + if key in self.valid_keys: + output.append(key) + return output + + if source_value not in self.valid_keys: + # Take first item from enum items + for item in self.enum_items: + for key in item.keys(): + source_value = key + break + return source_value + def set_override_state(self, *args, **kwargs): super(TaskTypeEnumEntity, 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 + + if self.multiselection: + new_value = [] + for key in self._current_value: + if key in self.valid_keys: + new_value.append(key) + + if self._current_value != new_value: + self.set(new_value) + else: + if not self.enum_items: + self.valid_keys.add("") + self.enum_items.append({"": "< Empty >"}) + + for item in self.enum_items: + for key in item.keys(): + value_on_not_set = key + break + + self.value_on_not_set = value_on_not_set + if ( + self._current_value is NOT_SET + or self._current_value not in self.valid_keys + ): + self.set(value_on_not_set) class ProvidersEnum(BaseEnumEntity): From 64e7dbb3475e326d999b094c88d2c9579b36eb4b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:46:29 +0200 Subject: [PATCH 071/115] fixed valid_value_types for providers --- openpype/settings/entities/enum_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 6c0e63fa1f..ee54bc6e02 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -456,7 +456,7 @@ class ProvidersEnum(BaseEnumEntity): self.value_on_not_set = "" self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (str, ) + self.valid_value_types = (STRING_TYPE, ) self.placeholder = None def _get_enum_values(self): From 8399de95cb9cb71e3d8b185267724aebb7256ce4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Sep 2021 16:51:11 +0200 Subject: [PATCH 072/115] nuke, resolve, hiero: precollector order lest then 0.5 --- openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_writes.py | 2 +- openpype/hosts/resolve/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/resolve/plugins/publish/precollect_workfile.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py index b0b171fb61..80c6abbaef 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py @@ -5,7 +5,7 @@ import pyblish.api class PreCollectClipEffects(pyblish.api.InstancePlugin): """Collect soft effects instances.""" - order = pyblish.api.CollectorOrder - 0.579 + order = pyblish.api.CollectorOrder - 0.479 label = "Precollect Clip Effects Instances" families = ["clip"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 9b529edf88..936ea2be58 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -13,7 +13,7 @@ from pprint import pformat class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Precollect Instances" hosts = ["hiero"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 530a433423..ff5d516065 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -12,7 +12,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.6 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index c2c25d0627..75d0b4f9a9 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -8,7 +8,7 @@ from avalon.nuke import lib as anlib class PreCollectNukeInstances(pyblish.api.ContextPlugin): """Collect all nodes with Avalon knob.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Pre-collect Instances" hosts = ["nuke", "nukeassist"] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 5d3eb5f609..8b1ccb8cef 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -9,7 +9,7 @@ reload(anlib) class CollectWorkfile(pyblish.api.ContextPlugin): """Collect current script for publish.""" - order = pyblish.api.CollectorOrder - 0.60 + order = pyblish.api.CollectorOrder - 0.50 label = "Pre-collect Workfile" hosts = ['nuke'] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 0b5fbc0479..47189c31fc 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -11,7 +11,7 @@ from avalon import io, api class CollectNukeWrites(pyblish.api.InstancePlugin): """Collect all write nodes.""" - order = pyblish.api.CollectorOrder - 0.58 + order = pyblish.api.CollectorOrder - 0.48 label = "Pre-collect Writes" hosts = ["nuke", "nukeassist"] families = ["write"] diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index 95b891d95a..8f1a13a4e5 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -8,7 +8,7 @@ from pprint import pformat class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Precollect Instances" hosts = ["resolve"] diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index ee05fb6f13..1333516177 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -13,7 +13,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Precollect the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.6 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): From d4a6db63f467c740d13af5fc644b6ee6ab669d0c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Sep 2021 11:41:26 +0200 Subject: [PATCH 073/115] Fix added underscore to internal methods --- .../plugins/publish/extract_harmony_zip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py index 85da01c890..adbac6ef09 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_harmony_zip.py @@ -66,10 +66,10 @@ class ExtractHarmonyZip(openpype.api.Extractor): # Get Task types and Statuses for creation if needed self.task_types = self._get_all_task_types(project_entity) - self.task_statuses = self.get_all_task_statuses(project_entity) + self.task_statuses = self._get_all_task_statuses(project_entity) # Get Statuses of AssetVersions - self.assetversion_statuses = self.get_all_assetversion_statuses( + self.assetversion_statuses = self._get_all_assetversion_statuses( project_entity ) From 478693d0ab6dbec4b76c8d9b80c6812766766895 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 11:47:23 +0200 Subject: [PATCH 074/115] added tine addon --- openpype/modules/example_addons/tiny_addon.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 openpype/modules/example_addons/tiny_addon.py diff --git a/openpype/modules/example_addons/tiny_addon.py b/openpype/modules/example_addons/tiny_addon.py new file mode 100644 index 0000000000..62962954f5 --- /dev/null +++ b/openpype/modules/example_addons/tiny_addon.py @@ -0,0 +1,9 @@ +from openpype.modules import OpenPypeAddOn + + +class TinyAddon(OpenPypeAddOn): + """This is tiniest possible addon. + + This addon won't do much but will exist in OpenPype modules environment. + """ + name = "tiniest_addon_ever" From 3dd69032510e8087a889c3bf38fbde96ce1204d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 11:53:59 +0200 Subject: [PATCH 075/115] added base of example addon --- .../example_addons/example_addon/__init__.py | 13 ++ .../example_addons/example_addon/addon.py | 124 ++++++++++++++++++ .../example_addon/interfaces.py | 28 ++++ .../plugins/publish/example_plugin.py | 10 ++ .../settings/defaults/project_settings.json | 1 + .../project_dynamic_schemas.json | 6 + .../system_dynamic_schemas.json | 6 + .../schemas/project_schemas/main.json | 29 ++++ .../schemas/project_schemas/the_template.json | 30 +++++ .../settings/schemas/system_schemas/main.json | 14 ++ .../example_addons/example_addon/widgets.py | 30 +++++ 11 files changed, 291 insertions(+) create mode 100644 openpype/modules/example_addons/example_addon/__init__.py create mode 100644 openpype/modules/example_addons/example_addon/addon.py create mode 100644 openpype/modules/example_addons/example_addon/interfaces.py create mode 100644 openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/widgets.py diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py new file mode 100644 index 0000000000..df4d61650b --- /dev/null +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -0,0 +1,13 @@ +""" Addon class definition and Settings definition must be imported here. + +If addon class or settings definition won't be here their definition won't +be found by OpenPype discovery. +""" + +from .addon import ( + AddonSettingsDef, +) + +__all__ = ( + "AddonSettingsDef", +) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py new file mode 100644 index 0000000000..64504be756 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -0,0 +1,124 @@ +"""Addon definition is located here. + +Import of python packages that may not be available should not be imported +in global space here until are required or used. +- Qt related imports +- imports of Python 3 packages + - we still support Python 2 hosts where addon definition should available +""" + +import os + +from openpype.modules import ( + JsonFilesSettingsDef, + OpenPypeAddOn +) +# Import interface defined by this addon to be able find other addons using it +from openpype_interfaces import ( + IExampleInterface, + IPluginPaths, + ITrayAction +) + + +# Settings definiton of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definiton using json files +# to define settings and store defaul values +class AddonSettingsDef(JsonFilesSettingsDef): + # This will add prefix to every schema and template from `schemas` + # subfolder. + # - it is not required to fill the prefix but it is highly + # recommended as schemas and templates may have name clashes across + # multiple addons + # - it is also recommended that prefix has addon name in it + schema_prefix = "addon_with_settings" + + def get_settings_root_path(self): + """Implemented abstract class of JsonFilesSettingsDef. + + Return directory path where json files defying addon settings are + located. + """ + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "settings" + ) + + +class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): + """This Addon has defined it's settings and interface. + + This example has system settings with enabled option. And use + few other interfaces: + - `IPluginPaths` to define custom plugin paths + - `ITrayAction` to be shown in tray tool + """ + label = "Example Addon" + name = "example_addon" + + def initialize(self, settings): + """Initialization of addon.""" + module_settings = settings[self.name] + # Enabled by settings + self.enabled = module_settings.get("enabled", False) + + # Prepare variables that can be used or set afterwards + self._connected_modules = None + # UI which must not be created at this time + self._dialog = None + + def connect_with_modules(self, enabled_modules): + """Method where you should find connected modules. + + It is triggered by OpenPype modules manager at the best possible time. + Some addons and modules may required to connect with other modules + before their main logic is executed so changes would require to restart + whole process. + """ + self._connected_modules = [] + for module in enabled_modules: + if isinstance(module, IExampleInterface): + self._connected_modules.append(module) + + def _create_dialog(self): + # Don't recreate dialog if already exists + if self._dialog is not None: + return + + from .widgets import MyExampleDialog + + self._dialog = MyExampleDialog() + + def show_dialog(self): + """Show dialog with connected modules. + + This can be called from anywhere but can also crash in headless mode. + There is not way how to prevent addon to do invalid operations if he's + not handling them. + """ + # Make sure dialog is created + self._create_dialog() + # Change value of dialog by current state + self._dialog.set_connected_modules(self.get_connected_modules()) + # Show dialog + self._dialog.open() + + def get_connected_modules(self): + """Custom implementation of addon.""" + names = set() + if self._connected_modules is not None: + for module in self._connected_modules: + names.add(module.name) + return names + + def on_action_trigger(self): + """Implementation of abstract method for `ITrayAction`.""" + self.show_dialog() + + def get_plugin_paths(self): + """Implementation of abstract method for `IPluginPaths`.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + + return { + "publish": [os.path.join(current_dir, "plugins", "publish")] + } diff --git a/openpype/modules/example_addons/example_addon/interfaces.py b/openpype/modules/example_addons/example_addon/interfaces.py new file mode 100644 index 0000000000..371536efc7 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/interfaces.py @@ -0,0 +1,28 @@ +""" Using interfaces is one way of connecting multiple OpenPype Addons/Modules. + +Interfaces must be in `interfaces.py` file (or folder). Interfaces should not +import module logic or other module in global namespace. That is because +all of them must be imported before all OpenPype AddOns and Modules. + +Ideally they should just define abstract and helper methods. If interface +require any logic or connection it should be defined in module. + +Keep in mind that attributes and methods will be added to other addon +attributes and methods so they should be unique and ideally contain +addon name in it's name. +""" + +from abc import abstractmethod +from openpype.modules import OpenPypeInterface + + +class IExampleInterface(OpenPypeInterface): + """Example interface of addon.""" + _example_module = None + + def get_example_module(self): + return self._example_module + + @abstractmethod + def example_method_of_example_interface(self): + pass diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py new file mode 100644 index 0000000000..8e7fb410bd --- /dev/null +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -0,0 +1,10 @@ +import os +import pyblish.api + + +class CollectExampleAddon(pyblish.api.ContextPlugin): + order = pyblish.api.CollectorOrder + 0.4 + label = "Collect Example Addon" + + def process(self, context): + self.log.info("I'm in example addon's plugin!") diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -0,0 +1 @@ +{} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json new file mode 100644 index 0000000000..f6b7d5d146 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "project_settings/global": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json new file mode 100644 index 0000000000..6895fb8f6d --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "system_settings/modules": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json new file mode 100644 index 0000000000..80e53ace7f --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "key": "exmaple_addon", + "collapsible": true, + "children": [ + { + "type": "number", + "key": "number", + "label": "This is your lucky number:", + "minimum": 7, + "maximum": 7, + "decimals": 0 + }, + { + "type": "template", + "name": "example_addon/the_template", + "template_data": [ + { + "name": "color_1", + "lable": "Color 1" + }, + { + "name": "color_2", + "lable": "Color 2" + } + ] + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json new file mode 100644 index 0000000000..af8fd9dae4 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json @@ -0,0 +1,30 @@ +[ + { + "type": "list-strict", + "key": "{name}", + "label": "{label}", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + } + ] + } +] diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json new file mode 100644 index 0000000000..0fb0a7c1be --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json @@ -0,0 +1,14 @@ +{ + "type": "dict", + "key": "example_addon", + "label": "Example addon", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py new file mode 100644 index 0000000000..8a74ad859f --- /dev/null +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -0,0 +1,30 @@ +from Qt import QtWidgets + + +class MyExampleDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(MyExampleDialog, self).__init__(parent) + + self.setWindowTitle("Connected modules") + + label_widget = QtWidgets.QLabel(self) + + ok_btn = QtWidgets.QPushButton("OK", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(label_widget) + layout.addLayout(btns_layout) + + self._label_widget = label_widget + + def set_connected_modules(self, connected_modules): + if connected_modules: + message = "\n".join(connected_modules) + else: + message = ( + "Other enabled modules/addons are not using my interface." + ) + self._label_widget.setText(message) From 53c6c9818454cb850751a58b8a6eec3907e075c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 12:08:49 +0200 Subject: [PATCH 076/115] #1938 - changed warning to info method Modified logging a bit --- openpype/lib/profiles_filtering.py | 7 ++++--- .../ftrack/plugins/publish/collect_ftrack_family.py | 3 --- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/lib/profiles_filtering.py b/openpype/lib/profiles_filtering.py index c4410204dd..992d757059 100644 --- a/openpype/lib/profiles_filtering.py +++ b/openpype/lib/profiles_filtering.py @@ -165,7 +165,8 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None): if match == -1: profile_value = profile.get(key) or [] logger.debug( - "\"{}\" not found in {}".format(key, profile_value) + "\"{}\" not found in \"{}\": {}".format(value, key, + profile_value) ) profile_points = -1 break @@ -192,13 +193,13 @@ def filter_profiles(profiles_data, key_values, keys_order=None, logger=None): ]) if not matching_profiles: - logger.warning( + logger.info( "None of profiles match your setup. {}".format(log_parts) ) return None if len(matching_profiles) > 1: - logger.warning( + logger.info( "More than one profile match your setup. {}".format(log_parts) ) diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py index cc2a5b7d37..70030acad9 100644 --- a/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -68,9 +68,6 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): instance.data["families"].append("ftrack") else: instance.data["families"] = ["ftrack"] - else: - self.log.debug("Instance '{}' doesn't match any profile".format( - instance.data.get("family"))) def _get_add_ftrack_f_from_addit_filters(self, additional_filters, From 8c37b2b1419d1b5e3065a4ebfe42e305165c1273 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:18 +0200 Subject: [PATCH 077/115] fixed settings and widget of example addon --- .../example_addons/example_addon/__init__.py | 2 ++ .../example_addons/example_addon/addon.py | 10 +++++++++- .../settings/defaults/project_settings.json | 16 +++++++++++++++- .../settings/defaults/system_settings.json | 5 +++++ .../dynamic_schemas/project_dynamic_schemas.json | 2 +- .../dynamic_schemas/system_dynamic_schemas.json | 2 +- .../settings/schemas/project_schemas/main.json | 7 ++++--- .../example_addons/example_addon/widgets.py | 9 +++++++++ 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py index df4d61650b..721d924436 100644 --- a/openpype/modules/example_addons/example_addon/__init__.py +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -6,8 +6,10 @@ be found by OpenPype discovery. from .addon import ( AddonSettingsDef, + ExampleAddon ) __all__ = ( "AddonSettingsDef", + "ExampleAddon" ) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 64504be756..5a25b80616 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -31,7 +31,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): # recommended as schemas and templates may have name clashes across # multiple addons # - it is also recommended that prefix has addon name in it - schema_prefix = "addon_with_settings" + schema_prefix = "example_addon" def get_settings_root_path(self): """Implemented abstract class of JsonFilesSettingsDef. @@ -67,6 +67,14 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): # UI which must not be created at this time self._dialog = None + def tray_init(self): + """Implementation of abstract method for `ITrayAction`. + + We're definetely in trat tool so we can precreate dialog. + """ + + self._create_dialog() + def connect_with_modules(self, enabled_modules): """Method where you should find connected modules. diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json index 0967ef424b..0a01fa8977 100644 --- a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -1 +1,15 @@ -{} +{ + "project_settings/example_addon": { + "number": 0, + "color_1": [ + 0.0, + 0.0, + 0.0 + ], + "color_2": [ + 0.0, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json new file mode 100644 index 0000000000..1e77356373 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json @@ -0,0 +1,5 @@ +{ + "modules/example_addon": { + "enabled": true + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json index f6b7d5d146..1f3da7b37f 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -1,6 +1,6 @@ { "project_settings/global": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json index 6895fb8f6d..6faa48ba74 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -1,6 +1,6 @@ { "system_settings/modules": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json index 80e53ace7f..ba692d860e 100644 --- a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -1,6 +1,7 @@ { "type": "dict", - "key": "exmaple_addon", + "key": "example_addon", + "label": "Example addon", "collapsible": true, "children": [ { @@ -17,11 +18,11 @@ "template_data": [ { "name": "color_1", - "lable": "Color 1" + "label": "Color 1" }, { "name": "color_2", - "lable": "Color 2" + "label": "Color 2" } ] } diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py index 8a74ad859f..0acf238409 100644 --- a/openpype/modules/example_addons/example_addon/widgets.py +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -1,5 +1,7 @@ from Qt import QtWidgets +from openpype.style import load_stylesheet + class MyExampleDialog(QtWidgets.QDialog): def __init__(self, parent=None): @@ -18,8 +20,15 @@ class MyExampleDialog(QtWidgets.QDialog): layout.addWidget(label_widget) layout.addLayout(btns_layout) + ok_btn.clicked.connect(self._on_ok_clicked) + self._label_widget = label_widget + self.setStyleSheet(load_stylesheet()) + + def _on_ok_clicked(self): + self.done(1) + def set_connected_modules(self, connected_modules): if connected_modules: message = "\n".join(connected_modules) From 4b0c8abcd52cd0254e4df73c314b98a2ec49f2fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:36 +0200 Subject: [PATCH 078/115] added new dynamic schema with name `system_settings/modules` --- .../entities/schemas/system_schema/schema_modules.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..4287dd7905 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -242,6 +242,10 @@ "label": "Enabled" } ] + }, + { + "type": "dynamic_schema", + "name": "system_settings/modules" } ] } From fa8383859cc0c3d01520a0e6213ff3caf3d65da5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:10 +0200 Subject: [PATCH 079/115] added ability to use schema as first children of dynamic schema definition --- openpype/settings/entities/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index f207322dee..bf3868c08d 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -168,9 +168,13 @@ class SchemasHub: if isinstance(def_schema, dict): def_schema = [def_schema] + all_def_schema = [] for item in def_schema: - item["_dynamic_schema_id"] = def_id - output.extend(def_schema) + items = self.resolve_schema_data(item) + for _item in items: + _item["_dynamic_schema_id"] = def_id + all_def_schema.extend(items) + output.extend(all_def_schema) return output def get_template_name(self, item_def, default=None): From 5bd1fa8a56b41913932df4aeb61a101109892c88 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:22 +0200 Subject: [PATCH 080/115] skip OpenPypeAddOn items too --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 01c3cebe60..2cd11e5b94 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -495,6 +495,7 @@ class ModulesManager: if ( not inspect.isclass(modules_item) or modules_item is OpenPypeModule + or modules_item is OpenPypeAddOn or not issubclass(modules_item, OpenPypeModule) ): continue From 7f7c7e00620e4a20143b6d1a2d14a51958a5fc9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:19:42 +0200 Subject: [PATCH 081/115] added few more information about addons settings to readme --- openpype/modules/README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a3733518ac..a6857b2c51 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -10,8 +10,6 @@ OpenPype modules should contain separated logic of specific kind of implementati - add module/addon manifest - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - defying that folder is content of a module or an addon -- module/addon have it's settings schemas and default values outside OpenPype -- add general setting of paths to modules ## Base class `OpenPypeModule` - abstract class as base for each module @@ -25,6 +23,26 @@ OpenPype modules should contain separated logic of specific kind of implementati - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces +## Addon class `OpenPypeAddOn` +- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) + +## How to add addons/modules +- in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder +- for openpype example addons use `{OPENPYPE_REPOS_ROOT}/openpype/modules/example_addons` + +## Addon/module settings +- addons/modules may have defined custom settings definitions with default values +- it is based on settings type `dynamic_schema` which has `name` + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherit from `BaseModuleSettingsDef` + - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons +- settings definition returns schemas by dynamic schemas names + # Interfaces - interface is class that has defined abstract methods to implement and may contain preimplemented helper methods - module that inherit from an interface must implement those abstract methods otherwise won't be initialized From 0932ea8aba1ec9af50fcbafa87b7022a7ac6dca0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 13:25:13 +0200 Subject: [PATCH 082/115] removed unsused import --- .../example_addon/plugins/publish/example_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py index 8e7fb410bd..695120e93b 100644 --- a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -1,4 +1,3 @@ -import os import pyblish.api From c6d781d7df7ab6e847328fc8188d77e29fedc941 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 13:34:56 +0200 Subject: [PATCH 083/115] #1976 - added methods to get configurable items for providers without use of Settings --- .../sync_server/sync_server_module.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index e65a410551..d2c70ec75a 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -403,6 +403,59 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """Wrapper for Local settings - all projects incl. Default""" return self.get_configurable_items(EditableScopes.LOCAL) + def get_system_configurable_items_for_provider(self, provider_name): + """ Gets system level configurable items without use of Setting + + Used for Setting UI to provide forms. + """ + scope = EditableScopes.SYSTEM + return self._get_configurable_items_for_provider(provider_name, scope) + + def get_project_configurable_items_for_provider(self, provider_name): + """ Gets project level configurable items without use of Setting + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.PROJECT + return self._get_configurable_items_for_provider(provider_name, scope) + + def get_system_configurable_items_for_providers(self): + """ Gets system level configurable items for all providers. + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.SYSTEM + ret_dict = {} + for provider_name in lib.factory.providers: + ret_dict[provider_name] = \ + self._get_configurable_items_for_provider(provider_name, scope) + + return ret_dict + + def get_project_configurable_items_for_providers(self): + """ Gets project level configurable items for all providers. + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.PROJECT + ret_dict = {} + for provider_name in lib.factory.providers: + ret_dict[provider_name] = \ + self._get_configurable_items_for_provider(provider_name, scope) + + return ret_dict + + def _get_configurable_items_for_provider(self, provider_name, scope): + items = lib.factory.get_provider_configurable_items(provider_name) + ret_dict = {} + + for item_key, item in items.items(): + if scope in item["scope"]: + item.pop("scope") + ret_dict[item_key] = item + + return ret_dict + def get_configurable_items(self, scope=None): """ Returns list of sites that could be configurable for all projects. From 3628ab8904b397fb71484e294c0d706bb22c8eda Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 13:51:53 +0200 Subject: [PATCH 084/115] fix changing of slider value from input field --- openpype/tools/settings/settings/item_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index a7b1208269..3b1fc061ec 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -445,7 +445,7 @@ class NumberWidget(InputWidget): value = self.input_field.value() if self._slider_widget is not None and not self._ignore_input_change: self._ignore_slider_change = True - self._slider_widget.setValue(value) + self._slider_widget.setValue(value * self._slider_multiplier) self._ignore_slider_change = False self.entity.set(value) From a4706062d3d345757a4248c2d650b19647553fe4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 14:50:55 +0200 Subject: [PATCH 085/115] #1976 - made new methods class methods --- .../sync_server/sync_server_module.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index d2c70ec75a..3e10ddac1d 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -403,23 +403,28 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """Wrapper for Local settings - all projects incl. Default""" return self.get_configurable_items(EditableScopes.LOCAL) - def get_system_configurable_items_for_provider(self, provider_name): + @classmethod + def get_system_configurable_items_for_provider(cls, provider_name): """ Gets system level configurable items without use of Setting Used for Setting UI to provide forms. """ scope = EditableScopes.SYSTEM - return self._get_configurable_items_for_provider(provider_name, scope) + return SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) - def get_project_configurable_items_for_provider(self, provider_name): + @classmethod + def get_project_configurable_items_for_provider(cls, provider_name): """ Gets project level configurable items without use of Setting It is not using Setting! Used for Setting UI to provide forms. """ scope = EditableScopes.PROJECT - return self._get_configurable_items_for_provider(provider_name, scope) + return SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) - def get_system_configurable_items_for_providers(self): + @classmethod + def get_system_configurable_items_for_providers(cls): """ Gets system level configurable items for all providers. It is not using Setting! Used for Setting UI to provide forms. @@ -428,11 +433,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - self._get_configurable_items_for_provider(provider_name, scope) + SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) return ret_dict - def get_project_configurable_items_for_providers(self): + @classmethod + def get_project_configurable_items_for_providers(cls): """ Gets project level configurable items for all providers. It is not using Setting! Used for Setting UI to provide forms. @@ -441,11 +448,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - self._get_configurable_items_for_provider(provider_name, scope) + SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) return ret_dict - def _get_configurable_items_for_provider(self, provider_name, scope): + @classmethod + def _get_configurable_items_for_provider(cls, provider_name, scope): items = lib.factory.get_provider_configurable_items(provider_name) ret_dict = {} From eef59e6fffb67671f752c6aef890c0c5d0ae5255 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 15:25:54 +0200 Subject: [PATCH 086/115] #1976 - standardize return to list --- .../sync_server/sync_server_module.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 3e10ddac1d..eeff5c499d 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -455,15 +455,24 @@ class SyncServerModule(OpenPypeModule, ITrayModule): @classmethod def _get_configurable_items_for_provider(cls, provider_name, scope): + """ + Args: + provider_name (str) + scope (EditableScopes) + Returns + (list) of (dict) + """ items = lib.factory.get_provider_configurable_items(provider_name) - ret_dict = {} - for item_key, item in items.items(): + ret = [] + for key in sorted(items.keys()): + item = items[key] if scope in item["scope"]: - item.pop("scope") - ret_dict[item_key] = item + item.pop("scope") # unneeded by UI + item.pop("namespace", None) # unneeded by UI + ret.append(item) - return ret_dict + return ret def get_configurable_items(self, scope=None): """ From 39341b1c2bb40caf038b5cb5595efcb1561ee047 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 15:32:14 +0200 Subject: [PATCH 087/115] #1976 - explicitly remove namespace in some cases Used cls instead of class name --- .../sync_server/sync_server_module.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index eeff5c499d..f0ae64d3fd 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -410,8 +410,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Used for Setting UI to provide forms. """ scope = EditableScopes.SYSTEM - return SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + return cls._get_configurable_items_for_provider(provider_name, scope) @classmethod def get_project_configurable_items_for_provider(cls, provider_name): @@ -420,8 +419,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): It is not using Setting! Used for Setting UI to provide forms. """ scope = EditableScopes.PROJECT - return SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + return cls._get_configurable_items_for_provider(provider_name, scope) @classmethod def get_system_configurable_items_for_providers(cls): @@ -433,8 +431,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + cls._get_configurable_items_for_provider(provider_name, scope) return ret_dict @@ -448,8 +445,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + cls._get_configurable_items_for_provider(provider_name, scope) return ret_dict @@ -469,7 +465,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): item = items[key] if scope in item["scope"]: item.pop("scope") # unneeded by UI - item.pop("namespace", None) # unneeded by UI + if scope in [EditableScopes.SYSTEM, EditableScopes.PROJECT]: + item.pop("namespace", None) # unneeded by UI ret.append(item) return ret From 94b3a182ef37d8d58f21026463fa0a00f50442e8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 11:50:20 +0200 Subject: [PATCH 088/115] #1976 - refactored, created simplified methods Commented out currently unneeded methods, not used anywhere, but logic could be salvaged after Settings will be modified --- .../providers/abstract_provider.py | 28 +- .../sync_server/providers/gdrive.py | 62 ++- .../sync_server/providers/lib.py | 8 + .../sync_server/providers/local_drive.py | 55 ++- .../sync_server/sync_server_module.py | 461 ++++++++---------- 5 files changed, 342 insertions(+), 272 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py index 2e9632134c..7fd25b9852 100644 --- a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py @@ -29,13 +29,35 @@ class AbstractProvider: @classmethod @abc.abstractmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties + Returns dict for editable properties on system settings level Returns: - (dict) + (list) of dict + """ + + @classmethod + @abc.abstractmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + + @classmethod + @abc.abstractmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + + + Returns: + (list) of dict """ @abc.abstractmethod diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 18d679b833..5db728f2de 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -96,30 +96,62 @@ class GDriveHandler(AbstractProvider): return self.service is not None @classmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties. + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + editable = [ + # credentials could be override on Project or User level + { + 'label': "Credentials url", + 'type': 'text', + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + }, + # roots could be override only on Project leve, User cannot + # + { + 'label': "Roots", + 'type': 'dict' + } + ] + return editable + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level Returns: (dict) """ - # {platform} tells that value is multiplatform and only specific OS - # should be returned - editable = { + editable = [ # credentials could be override on Project or User level - 'credentials_url': { - 'scope': [EditableScopes.PROJECT, - EditableScopes.LOCAL], + { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 - }, - # roots could be override only on Project leve, User cannot - 'root': {'scope': [EditableScopes.PROJECT], - 'label': "Roots", - 'type': 'dict'} - } + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + } + ] return editable def get_roots_config(self, anatomy=None): diff --git a/openpype/modules/default_modules/sync_server/providers/lib.py b/openpype/modules/default_modules/sync_server/providers/lib.py index 816ccca981..463e49dd4d 100644 --- a/openpype/modules/default_modules/sync_server/providers/lib.py +++ b/openpype/modules/default_modules/sync_server/providers/lib.py @@ -76,6 +76,14 @@ class ProviderFactory: return provider_info[0].get_configurable_items() + def get_provider_cls(self, provider_code): + """ + Returns class object for 'provider_code' to run class methods on. + """ + provider_info = self._get_creator_info(provider_code) + + return provider_info[0] + def _get_creator_info(self, provider): """ Collect all necessary info for provider. Currently only creator diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 4b80ed44f2..b3482ac1d8 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -30,18 +30,59 @@ class LocalDriveHandler(AbstractProvider): return True @classmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + editable = [ + # credentials could be override on Project or User level + { + 'label': "Credentials url", + 'type': 'text', + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + }, + # roots could be override only on Project leve, User cannot + # + { + 'label': "Roots", + 'type': 'dict' + } + ] + return editable + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + Returns: (dict) """ - editable = { - 'root': {'scope': [EditableScopes.LOCAL], - 'label': "Roots", - 'type': 'dict'} - } + editable = [ + { + 'label': "Roots", + 'type': 'dict' + } + ] return editable def upload_file(self, source_path, target_path, diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index f0ae64d3fd..2eb749801e 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -8,7 +8,7 @@ import copy from avalon.api import AvalonMongoDB from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayModule +from openpype.modules.default_modules.interfaces import ITrayModule from openpype.api import ( Anatomy, get_project_settings, @@ -399,272 +399,239 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return remote_site - def get_local_settings_schema(self): - """Wrapper for Local settings - all projects incl. Default""" - return self.get_configurable_items(EditableScopes.LOCAL) - + # Methods for Settings UI to draw appropriate forms @classmethod - def get_system_configurable_items_for_provider(cls, provider_name): - """ Gets system level configurable items without use of Setting + def get_system_settings_schema(cls): + """ Gets system level schema of configurable items Used for Setting UI to provide forms. """ - scope = EditableScopes.SYSTEM - return cls._get_configurable_items_for_provider(provider_name, scope) - - @classmethod - def get_project_configurable_items_for_provider(cls, provider_name): - """ Gets project level configurable items without use of Setting - - It is not using Setting! Used for Setting UI to provide forms. - """ - scope = EditableScopes.PROJECT - return cls._get_configurable_items_for_provider(provider_name, scope) - - @classmethod - def get_system_configurable_items_for_providers(cls): - """ Gets system level configurable items for all providers. - - It is not using Setting! Used for Setting UI to provide forms. - """ - scope = EditableScopes.SYSTEM ret_dict = {} - for provider_name in lib.factory.providers: - ret_dict[provider_name] = \ - cls._get_configurable_items_for_provider(provider_name, scope) + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_system_settings_schema() return ret_dict @classmethod - def get_project_configurable_items_for_providers(cls): - """ Gets project level configurable items for all providers. + def get_project_settings_schema(cls): + """ Gets project level schema of configurable items. It is not using Setting! Used for Setting UI to provide forms. """ - scope = EditableScopes.PROJECT ret_dict = {} - for provider_name in lib.factory.providers: - ret_dict[provider_name] = \ - cls._get_configurable_items_for_provider(provider_name, scope) + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_project_settings_schema() return ret_dict @classmethod - def _get_configurable_items_for_provider(cls, provider_name, scope): + def get_local_settings_schema(cls): + """ Gets local level schema of configurable items. + + It is not using Setting! Used for Setting UI to provide forms. """ - Args: - provider_name (str) - scope (EditableScopes) - Returns - (list) of (dict) - """ - items = lib.factory.get_provider_configurable_items(provider_name) + ret_dict = {} + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_local_settings_schema() - ret = [] - for key in sorted(items.keys()): - item = items[key] - if scope in item["scope"]: - item.pop("scope") # unneeded by UI - if scope in [EditableScopes.SYSTEM, EditableScopes.PROJECT]: - item.pop("namespace", None) # unneeded by UI - ret.append(item) + return ret_dict - return ret - - def get_configurable_items(self, scope=None): - """ - Returns list of sites that could be configurable for all projects. - - Could be filtered by 'scope' argument (list) - - Args: - scope (list of utils.EditableScope) - - Returns: - (dict of list of dict) - { - siteA : [ - { - key:"root", label:"root", - "value":"{'work': 'c:/projects'}", - "type": "dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, - { - key:"credentials_url", label:"Credentials url", - "value":"'c:/projects/cred.json'", "type": "text", - "namespace": "{project_setting}/global/sync_server/ - sites" - } - ] - } - """ - editable = {} - applicable_projects = list(self.connection.projects()) - applicable_projects.append(None) - for project in applicable_projects: - project_name = None - if project: - project_name = project["name"] - - items = self.get_configurable_items_for_project(project_name, - scope) - editable.update(items) - - return editable - - def get_local_settings_schema_for_project(self, project_name): - """Wrapper for Local settings - for specific 'project_name'""" - return self.get_configurable_items_for_project(project_name, - EditableScopes.LOCAL) - - def get_configurable_items_for_project(self, project_name=None, - scope=None): - """ - Returns list of items that could be configurable for specific - 'project_name' - - Args: - project_name (str) - None > default project, - scope (list of utils.EditableScope) - (optional, None is all scopes, default is LOCAL) - - Returns: - (dict of list of dict) - { - siteA : [ - { - key:"root", label:"root", - "type": "dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, - { - key:"credentials_url", label:"Credentials url", - "value":"'c:/projects/cred.json'", "type": "text", - "namespace": "{project_setting}/global/sync_server/ - sites" - } - ] - } - """ - allowed_sites = set() - sites = self.get_all_site_configs(project_name) - if project_name: - # Local Settings can select only from allowed sites for project - allowed_sites.update(set(self.get_active_sites(project_name))) - allowed_sites.update(set(self.get_remote_sites(project_name))) - - editable = {} - for site_name in sites.keys(): - if allowed_sites and site_name not in allowed_sites: - continue - - items = self.get_configurable_items_for_site(project_name, - site_name, - scope) - # Local Settings need 'local' instead of real value - site_name = site_name.replace(get_local_site_id(), 'local') - editable[site_name] = items - - return editable - - def get_local_settings_schema_for_site(self, project_name, site_name): - """Wrapper for Local settings - for particular 'site_name and proj.""" - return self.get_configurable_items_for_site(project_name, - site_name, - EditableScopes.LOCAL) - - def get_configurable_items_for_site(self, project_name=None, - site_name=None, - scope=None): - """ - Returns list of items that could be configurable. - - Args: - project_name (str) - None > default project - site_name (str) - scope (list of utils.EditableScope) - (optional, None is all scopes) - - Returns: - (list) - [ - { - key:"root", label:"root", type:"dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, ... - ] - """ - provider_name = self.get_provider_for_site(site=site_name) - items = lib.factory.get_provider_configurable_items(provider_name) - - if project_name: - sync_s = self.get_sync_project_setting(project_name, - exclude_locals=True, - cached=False) - else: - sync_s = get_default_project_settings(exclude_locals=True) - sync_s = sync_s["global"]["sync_server"] - sync_s["sites"].update( - self._get_default_site_configs(self.enabled)) - - editable = [] - if type(scope) is not list: - scope = [scope] - scope = set(scope) - for key, properties in items.items(): - if scope is None or scope.intersection(set(properties["scope"])): - val = sync_s.get("sites", {}).get(site_name, {}).get(key) - - item = { - "key": key, - "label": properties["label"], - "type": properties["type"] - } - - if properties.get("namespace"): - item["namespace"] = properties.get("namespace") - if "platform" in item["namespace"]: - try: - if val: - val = val[platform.system().lower()] - except KeyError: - st = "{}'s field value {} should be".format(key, val) # noqa: E501 - log.error(st + " multiplatform dict") - - item["namespace"] = item["namespace"].replace('{site}', - site_name) - children = [] - if properties["type"] == "dict": - if val: - for val_key, val_val in val.items(): - child = { - "type": "text", - "key": val_key, - "value": val_val - } - children.append(child) - - if properties["type"] == "dict": - item["children"] = children - else: - item["value"] = val - - editable.append(item) - - return editable + # Needs to be refactored after Settings are updated + # # Methods for Settings to get appriate values to fill forms + # def get_configurable_items(self, scope=None): + # """ + # Returns list of sites that could be configurable for all projects. + # + # Could be filtered by 'scope' argument (list) + # + # Args: + # scope (list of utils.EditableScope) + # + # Returns: + # (dict of list of dict) + # { + # siteA : [ + # { + # key:"root", label:"root", + # "value":"{'work': 'c:/projects'}", + # "type": "dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, + # { + # key:"credentials_url", label:"Credentials url", + # "value":"'c:/projects/cred.json'", "type": "text", + # "namespace": "{project_setting}/global/sync_server/ + # sites" + # } + # ] + # } + # """ + # editable = {} + # applicable_projects = list(self.connection.projects()) + # applicable_projects.append(None) + # for project in applicable_projects: + # project_name = None + # if project: + # project_name = project["name"] + # + # items = self.get_configurable_items_for_project(project_name, + # scope) + # editable.update(items) + # + # return editable + # + # def get_local_settings_schema_for_project(self, project_name): + # """Wrapper for Local settings - for specific 'project_name'""" + # return self.get_configurable_items_for_project(project_name, + # EditableScopes.LOCAL) + # + # def get_configurable_items_for_project(self, project_name=None, + # scope=None): + # """ + # Returns list of items that could be configurable for specific + # 'project_name' + # + # Args: + # project_name (str) - None > default project, + # scope (list of utils.EditableScope) + # (optional, None is all scopes, default is LOCAL) + # + # Returns: + # (dict of list of dict) + # { + # siteA : [ + # { + # key:"root", label:"root", + # "type": "dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, + # { + # key:"credentials_url", label:"Credentials url", + # "value":"'c:/projects/cred.json'", "type": "text", + # "namespace": "{project_setting}/global/sync_server/ + # sites" + # } + # ] + # } + # """ + # allowed_sites = set() + # sites = self.get_all_site_configs(project_name) + # if project_name: + # # Local Settings can select only from allowed sites for project + # allowed_sites.update(set(self.get_active_sites(project_name))) + # allowed_sites.update(set(self.get_remote_sites(project_name))) + # + # editable = {} + # for site_name in sites.keys(): + # if allowed_sites and site_name not in allowed_sites: + # continue + # + # items = self.get_configurable_items_for_site(project_name, + # site_name, + # scope) + # # Local Settings need 'local' instead of real value + # site_name = site_name.replace(get_local_site_id(), 'local') + # editable[site_name] = items + # + # return editable + # + # def get_configurable_items_for_site(self, project_name=None, + # site_name=None, + # scope=None): + # """ + # Returns list of items that could be configurable. + # + # Args: + # project_name (str) - None > default project + # site_name (str) + # scope (list of utils.EditableScope) + # (optional, None is all scopes) + # + # Returns: + # (list) + # [ + # { + # key:"root", label:"root", type:"dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, ... + # ] + # """ + # provider_name = self.get_provider_for_site(site=site_name) + # items = lib.factory.get_provider_configurable_items(provider_name) + # + # if project_name: + # sync_s = self.get_sync_project_setting(project_name, + # exclude_locals=True, + # cached=False) + # else: + # sync_s = get_default_project_settings(exclude_locals=True) + # sync_s = sync_s["global"]["sync_server"] + # sync_s["sites"].update( + # self._get_default_site_configs(self.enabled)) + # + # editable = [] + # if type(scope) is not list: + # scope = [scope] + # scope = set(scope) + # for key, properties in items.items(): + # if scope is None or scope.intersection(set(properties["scope"])): + # val = sync_s.get("sites", {}).get(site_name, {}).get(key) + # + # item = { + # "key": key, + # "label": properties["label"], + # "type": properties["type"] + # } + # + # if properties.get("namespace"): + # item["namespace"] = properties.get("namespace") + # if "platform" in item["namespace"]: + # try: + # if val: + # val = val[platform.system().lower()] + # except KeyError: + # st = "{}'s field value {} should be".format(key, val) # noqa: E501 + # log.error(st + " multiplatform dict") + # + # item["namespace"] = item["namespace"].replace('{site}', + # site_name) + # children = [] + # if properties["type"] == "dict": + # if val: + # for val_key, val_val in val.items(): + # child = { + # "type": "text", + # "key": val_key, + # "value": val_val + # } + # children.append(child) + # + # if properties["type"] == "dict": + # item["children"] = children + # else: + # item["value"] = val + # + # editable.append(item) + # + # return editable def reset_timer(self): """ From 3f2a4d5a5aa5f8d037d55551a127e86990b60c70 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 11:59:40 +0200 Subject: [PATCH 089/115] Hound --- .../default_modules/sync_server/sync_server_module.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 2eb749801e..39b5c9314e 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -8,7 +8,7 @@ import copy from avalon.api import AvalonMongoDB from openpype.modules import OpenPypeModule -from openpype.modules.default_modules.interfaces import ITrayModule +from openpype_interfaces import ITrayModule from openpype.api import ( Anatomy, get_project_settings, @@ -16,14 +16,13 @@ from openpype.api import ( get_local_site_id) from openpype.lib import PypeLogger from openpype.settings.lib import ( - get_default_project_settings, get_default_anatomy_settings, get_anatomy_settings) from .providers.local_drive import LocalDriveHandler from .providers import lib -from .utils import time_function, SyncStatus, EditableScopes +from .utils import time_function, SyncStatus log = PypeLogger().get_logger("SyncServer") @@ -646,7 +645,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): enabled_projects = [] if self.enabled: - for project in self.connection.projects(): + for project in self.connection.projects(projection={"name": 1}): project_name = project["name"] project_settings = self.get_sync_project_setting(project_name) if project_settings and project_settings.get("enabled"): From 7d37971d64f57408b22fc582e1a388a99cc28c3a Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 15:47:38 +0200 Subject: [PATCH 090/115] get last version string from path This changes get_version_from_path() to produce the same result as version_up --- openpype/lib/path_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index e1dd1e7f10..88bb1a216a 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -77,7 +77,7 @@ def get_version_from_path(file): """ pattern = re.compile(r"[\._]v([0-9]+)", re.IGNORECASE) try: - return pattern.findall(file)[0] + return pattern.findall(file)[-1] except IndexError: log.error( "templates:get_version_from_workfile:" From 7a88a4ac1326796b6224e5547a3656b2a5b9032c Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 16:11:30 +0200 Subject: [PATCH 091/115] Makes thumbnail from the middle of the clip If read node frame range is 1001-1010, thumbnail is now made from frame 1005, not 505. --- .../nuke/plugins/publish/extract_thumbnail.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index da30dcc632..6921d6e9b3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -112,12 +112,12 @@ class ExtractThumbnail(openpype.api.Extractor): # create write node write_node = nuke.createNode("Write") - file = fhead + "jpeg" + file = fhead + "jpg" name = "thumbnail" path = os.path.join(staging_dir, file).replace("\\", "/") instance.data["thumbnail"] = path write_node["file"].setValue(path) - write_node["file_type"].setValue("jpeg") + write_node["file_type"].setValue("jpg") write_node["raw"].setValue(1) write_node.setInput(0, previous_node) temporary_nodes.append(write_node) @@ -126,10 +126,11 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 + mid_frame = int((int(last_frame) - int(first_frame) ) / 2) + int(first_frame) repre = { 'name': name, - 'ext': "jpeg", + 'ext': "jpg", "outputName": "thumb", 'files': file, "stagingDir": staging_dir, @@ -140,7 +141,7 @@ class ExtractThumbnail(openpype.api.Extractor): instance.data["representations"].append(repre) # Render frames - nuke.execute(write_node.name(), int(first_frame), int(last_frame)) + nuke.execute(write_node.name(), int(mid_frame), int(mid_frame)) self.log.debug( "representations: {}".format(instance.data["representations"])) @@ -157,12 +158,12 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() - if "VIEWER_INPUT" not in ipn and ip: - ipn_orig = nuke.toNode(ipn) - ipn_orig.setSelected(True) + if "Viewer" == n.Class()]: + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) if ipn_orig: nuke.nodeCopy('%clipboard%') From 0518064e96ef6b3a714988dfe88c6061173795f2 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:30:17 +0200 Subject: [PATCH 092/115] indent change --- .../hosts/nuke/plugins/publish/extract_thumbnail.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 6921d6e9b3..93a5c9b51d 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,11 +159,11 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() if "Viewer" == n.Class()]: - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() - if "VIEWER_INPUT" not in ipn and ip: - ipn_orig = nuke.toNode(ipn) - ipn_orig.setSelected(True) + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) if ipn_orig: nuke.nodeCopy('%clipboard%') From 81432101164a3731c65e6d60b9880029a28e8202 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:33:32 +0200 Subject: [PATCH 093/115] long line fix --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 93a5c9b51d..dfb26aab80 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -126,7 +126,7 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 - mid_frame = int((int(last_frame) - int(first_frame) ) / 2) + int(first_frame) + mid_frame = int((int(last_frame)-int(first_frame))/2)+int(first_frame) repre = { 'name': name, From 25a7c5d0f0d15525d1fe58ffdc8f120558eac462 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:38:29 +0200 Subject: [PATCH 094/115] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index dfb26aab80..4c21891f48 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -126,7 +126,8 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 - mid_frame = int((int(last_frame)-int(first_frame))/2)+int(first_frame) + mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ + + int(first_frame) repre = { 'name': name, From 17fd666a49a374a895a4c6913cb8f326c411de56 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:40:58 +0200 Subject: [PATCH 095/115] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 4c21891f48..c0dc1417c3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -127,7 +127,7 @@ class ExtractThumbnail(openpype.api.Extractor): first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ - + int(first_frame) + + int(first_frame) repre = { 'name': name, From 1ab40ddd244b27db31fe639cc63e15096301e1e9 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:43:31 +0200 Subject: [PATCH 096/115] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index c0dc1417c3..7ffeec2db9 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -124,10 +124,10 @@ class ExtractThumbnail(openpype.api.Extractor): tags = ["thumbnail", "publish_on_farm"] # retime for - first_frame = int(last_frame) / 2 - last_frame = int(last_frame) / 2 mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ + int(first_frame) + first_frame = int(last_frame) / 2 + last_frame = int(last_frame) / 2 repre = { 'name': name, From ca3034f2725fb1be97b37850f8a7d1ab0e017be7 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:46:09 +0200 Subject: [PATCH 097/115] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 7ffeec2db9..b9d6762880 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,7 +159,7 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From e3b319ffc66fc24668480fc3405bb05aa51089c2 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:58:31 +0200 Subject: [PATCH 098/115] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index b9d6762880..55f7b746fc 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,7 +159,7 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From 5a425a5269c57496dfbd02adbaedbc969a812e22 Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Wed, 8 Sep 2021 16:09:40 -0700 Subject: [PATCH 099/115] Moving project folder structure creation out of ftrack module #1989 --- openpype/api.py | 11 ++- openpype/lib/__init__.py | 6 +- openpype/lib/path_tools.py | 43 ++++++++++ .../action_create_project_structure.py | 79 +++---------------- openpype/settings/__init__.py | 5 +- openpype/settings/lib.py | 29 +++++++ 6 files changed, 98 insertions(+), 75 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index ce18097eca..dcff127e9f 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -4,6 +4,7 @@ from .settings import ( get_current_project_settings, get_anatomy_settings, get_environments, + get_project_basic_paths, SystemSettings, ProjectSettings @@ -24,7 +25,8 @@ from .lib import ( get_latest_version, get_global_environments, get_local_site_id, - change_openpype_mongo_url + change_openpype_mongo_url, + create_project_folders ) from .lib.mongo import ( @@ -72,6 +74,7 @@ __all__ = [ "get_current_project_settings", "get_anatomy_settings", "get_environments", + "get_project_basic_paths", "SystemSettings", @@ -120,5 +123,9 @@ __all__ = [ "get_global_environments", "get_local_site_id", - "change_openpype_mongo_url" + "change_openpype_mongo_url", + + "get_project_basic_paths", + "create_project_folders" + ] diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 12c04a4236..886d61fb39 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -139,7 +139,8 @@ from .plugin_tools import ( from .path_tools import ( version_up, get_version_from_path, - get_last_version_from_path + get_last_version_from_path, + create_project_folders ) from .editorial import ( @@ -268,5 +269,6 @@ __all__ = [ "range_from_frames", "frames_to_secons", "frames_to_timecode", - "make_sequence_collection" + "make_sequence_collection", + "create_project_folders" ] diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index e1dd1e7f10..fab0879759 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -2,8 +2,12 @@ import os import re import logging +from openpype.api import Anatomy + log = logging.getLogger(__name__) +pattern_array = re.compile(r"\[.*\]") +project_root_key = "__project_root__" def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" @@ -119,3 +123,42 @@ def get_last_version_from_path(path_dir, filter): return filtred_files[-1] return None + + +def compute_paths(basic_paths_items, project_root): + output = [] + for path_items in basic_paths_items: + clean_items = [] + for path_item in path_items: + matches = re.findall(pattern_array, path_item) + if len(matches) > 0: + path_item = path_item.replace(matches[0], "") + if path_item == project_root_key: + path_item = project_root + clean_items.append(path_item) + output.append(os.path.normpath(os.path.sep.join(clean_items))) + return output + + +def create_project_folders(basic_paths, project_name): + anatomy = Anatomy(project_name) + roots_paths = [] + if isinstance(anatomy.roots, dict): + for root in anatomy.roots.values(): + roots_paths.append(root.value) + else: + roots_paths.append(anatomy.roots.value) + + for root_path in roots_paths: + project_root = os.path.join(root_path, project_name) + full_paths = compute_paths(basic_paths, project_root) + # Create folders + for path in full_paths: + full_path = path.format(project_root=project_root) + if os.path.exists(full_path): + log.debug( + "Folder already exists: {}".format(full_path) + ) + else: + log.debug("Creating folder: {}".format(full_path)) + os.makedirs(full_path) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py index 035a1c60de..b0de792473 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py @@ -3,7 +3,7 @@ import re import json from openpype.modules.ftrack.lib import BaseAction, statics_icon -from openpype.api import Anatomy, get_project_settings +from openpype.api import get_project_basic_paths, create_project_folders class CreateProjectFolders(BaseAction): @@ -72,25 +72,18 @@ class CreateProjectFolders(BaseAction): def launch(self, session, entities, event): # Get project entity project_entity = self.get_project_from_entity(entities[0]) - # Load settings for project project_name = project_entity["full_name"] - project_settings = get_project_settings(project_name) - project_folder_structure = ( - project_settings["global"]["project_folder_structure"] - ) - if not project_folder_structure: - return { - "success": False, - "message": "Project structure is not set." - } - try: - if isinstance(project_folder_structure, str): - project_folder_structure = json.loads(project_folder_structure) - # Get paths based on presets - basic_paths = self.get_path_items(project_folder_structure) - self.create_folders(basic_paths, project_entity) + basic_paths = get_project_basic_paths(project_name) + if not basic_paths: + return { + "success": False, + "message": "Project structure is not set." + } + + # Invoking OpenPype API to create the project folders + create_project_folders(basic_paths, project_name) self.create_ftrack_entities(basic_paths, project_entity) except Exception as exc: @@ -195,58 +188,6 @@ class CreateProjectFolders(BaseAction): self.session.commit() return new_ent - def get_path_items(self, in_dict): - output = [] - for key, value in in_dict.items(): - if not value: - output.append(key) - else: - paths = self.get_path_items(value) - for path in paths: - if not isinstance(path, (list, tuple)): - path = [path] - - output.append([key, *path]) - - return output - - def compute_paths(self, basic_paths_items, project_root): - output = [] - for path_items in basic_paths_items: - clean_items = [] - for path_item in path_items: - matches = re.findall(self.pattern_array, path_item) - if len(matches) > 0: - path_item = path_item.replace(matches[0], "") - if path_item == self.project_root_key: - path_item = project_root - clean_items.append(path_item) - output.append(os.path.normpath(os.path.sep.join(clean_items))) - return output - - def create_folders(self, basic_paths, project): - anatomy = Anatomy(project["full_name"]) - roots_paths = [] - if isinstance(anatomy.roots, dict): - for root in anatomy.roots.values(): - roots_paths.append(root.value) - else: - roots_paths.append(anatomy.roots.value) - - for root_path in roots_paths: - project_root = os.path.join(root_path, project["full_name"]) - full_paths = self.compute_paths(basic_paths, project_root) - # Create folders - for path in full_paths: - full_path = path.format(project_root=project_root) - if os.path.exists(full_path): - self.log.debug( - "Folder already exists: {}".format(full_path) - ) - else: - self.log.debug("Creating folder: {}".format(full_path)) - os.makedirs(full_path) - def register(session): CreateProjectFolders(session).register() diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index b5810deef4..1905b6dc73 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -7,7 +7,8 @@ from .lib import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_local_settings + get_local_settings, + get_project_basic_paths ) from .entities import ( SystemSettings, @@ -24,7 +25,7 @@ __all__ = ( "get_anatomy_settings", "get_environments", "get_local_settings", - + "get_project_basic_paths", "SystemSettings", "ProjectSettings" ) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 5c2c0dcd94..6d8ece1b53 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -901,6 +901,35 @@ def get_general_environments(): return environments +def _list_path_items(folder_structure): + output = [] + for key, value in folder_structure.items(): + if not value: + output.append(key) + else: + paths = _list_path_items(value) + for path in paths: + if not isinstance(path, (list, tuple)): + path = [path] + + output.append([key, *path]) + + return output + + +def get_project_basic_paths(project_name): + project_settings = get_project_settings(project_name) + folder_structure = ( + project_settings["global"]["project_folder_structure"] + ) + if not folder_structure: + return [] + + if isinstance(folder_structure, str): + folder_structure = json.loads(folder_structure) + return _list_path_items(folder_structure) + + def clear_metadata_from_settings(values): """Remove all metadata keys from loaded settings.""" if isinstance(values, dict): From 46f38b1e02888769f93727ae77baedc6182c5c26 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:18:02 +0200 Subject: [PATCH 100/115] Fix typos --- openpype/modules/README.md | 168 ++++++++++++++++++------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a6857b2c51..abc7ed3961 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -1,31 +1,31 @@ # OpenPype modules/addons -OpenPype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering or may contain only special plugins. Addons work the same way currently there is no difference in module and addon. +OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its usage code, Deadline farm rendering or may contain only special plugins. Addons work the same way currently, there is no difference between module and addon functionality. ## Modules concept -- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the modulo located -- modules or addons should never be imported directly even if you know possible full import path - - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts +- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the module located +- modules or addons should never be imported directly, even if you know possible full import path + - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts ### TODOs - add module/addon manifest - - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - - defying that folder is content of a module or an addon + - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) + - defying that folder is content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module -- implementation should be module's api withou GUI parts -- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths) +- implementation should contain module's api without GUI parts +- may implement `get_global_environments` method which should return dictionary of environments that are globally applicable and value is the same for whole studio if launched at any workstation (except os specific paths) - abstract parts: - - `name` attribute - name of a module - - `initialize` method - method for own initialization of a module (should not override `__init__`) - - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules -- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module - - also keep in mind that they may be initialized in headless mode + - `name` attribute - name of a module + - `initialize` method - method for own initialization of a module (should not override `__init__`) + - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules +- `__init__` should not be overridden and `initialize` should not do time consuming part but only prepare base data about module + - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces ## Addon class `OpenPypeAddOn` -- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods - - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) +- inherits from `OpenPypeModule` but is enabled by default and doesn't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) ## How to add addons/modules - in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder @@ -34,110 +34,110 @@ OpenPype modules should contain separated logic of specific kind of implementati ## Addon/module settings - addons/modules may have defined custom settings definitions with default values - it is based on settings type `dynamic_schema` which has `name` - - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults - - they can't be added to any schema hierarchy - - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) - - addons may define it's dynamic schema items -- they can be defined with class which inherit from `BaseModuleSettingsDef` - - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values - - check it's docstring and check for `example_addon` in example addons + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherits from `BaseModuleSettingsDef` + - it is recommended to use pre implemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons - settings definition returns schemas by dynamic schemas names # Interfaces -- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods +- interface is class that has defined abstract methods to implement and may contain pre implemented helper methods - module that inherit from an interface must implement those abstract methods otherwise won't be initialized -- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods +- it is easy to find which module object inherited from which interfaces with 100% chance they have implemented required methods - interfaces can be defined in `interfaces.py` inside module directory - - the file can't use relative imports or import anything from other parts - of module itself at the header of file - - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation + - the file can't use relative imports or import anything from other parts + of module itself at the header of file + - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation ## Base class `OpenPypeInterface` - has nothing implemented - has ABCMeta as metaclass - is defined to be able find out classes which inherit from this base to be - able tell this is an Interface + able tell this is an Interface ## Global interfaces - few interfaces are implemented for global usage ### IPluginPaths -- module want to add directory path/s to avalon or publish plugins +- module wants to add directory path/s to avalon or publish plugins - module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"` - - each key may contain list or string with path to directory with plugins + - each key may contain list or string with a path to directory with plugins ### ITrayModule -- module has more logic when used in tray - - it is possible that module can be used only in tray +- module has more logic when used in a tray + - it is possible that module can be used only in the tray - abstract methods - - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` - - `tray_menu` - add actions to tray widget's menu that represent the module - - `tray_start` - start of module's login in tray - - module is initialized and connected with other modules - - `tray_exit` - module's cleanup like stop and join threads etc. - - order of calling is based on implementation this order is how it works with `TrayModulesManager` - - it is recommended to import and use GUI implementaion only in these methods + - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` + - `tray_menu` - add actions to tray widget's menu that represent the module + - `tray_start` - start of module's login in tray + - module is initialized and connected with other modules + - `tray_exit` - module's cleanup like stop and join threads etc. + - order of calling is based on implementation this order is how it works with `TrayModulesManager` + - it is recommended to import and use GUI implementation only in these methods - has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init` - - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations + - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations ### ITrayService -- inherit from `ITrayModule` and implement `tray_menu` method for you - - add action to submenu "Services" in tray widget menu with icon and label -- abstract atttribute `label` - - label shown in menu -- interface has preimplemented methods to change icon color - - `set_service_running` - green icon - - `set_service_failed` - red icon - - `set_service_idle` - orange icon - - these states must be set by module itself `set_service_running` is default state on initialization +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to submenu "Services" in tray widget menu with icon and label +- abstract attribute `label` + - label shown in menu +- interface has pre implemented methods to change icon color + - `set_service_running` - green icon + - `set_service_failed` - red icon + - `set_service_idle` - orange icon + - these states must be set by module itself `set_service_running` is default state on initialization ### ITrayAction -- inherit from `ITrayModule` and implement `tray_menu` method for you - - add action to tray widget menu with label -- abstract atttribute `label` - - label shown in menu +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to tray widget menu with label +- abstract attribute `label` + - label shown in menu - abstract method `on_action_trigger` - - what should happen when action is triggered -- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray + - what should happen when an action is triggered +- NOTE: It is a good idea to implement logic in `on_action_trigger` to the api method and trigger that method on callbacks. This gives ability to trigger that method outside tray ## Modules interfaces -- modules may have defined their interfaces to be able recognize other modules that would want to use their features -- -### Example: -- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers - - Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers +- modules may have defined their own interfaces to be able to recognize other modules that would want to use their features -- Clockify has more inharitance it's class definition looks like +### Example: +- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which other modules want to add paths to server/user event handlers + - Clockify module use `IFtrackEventHandlerPaths` and returns paths to clockify ftrack synchronizers + +- Clockify inherits from more interfaces. It's class definition looks like: ``` class ClockifyModule( - OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize. - ITrayModule, # Says has special implementation when used in tray. - IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). - IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. - ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. + OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize. + ITrayModule, # Says has special implementation when used in tray. + IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). + IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. + ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. ): ``` ### ModulesManager -- collect module classes and tries to initialize them +- collects module classes and tries to initialize them - important attributes - - `modules` - list of available attributes - - `modules_by_id` - dictionary of modules mapped by their ids - - `modules_by_name` - dictionary of modules mapped by their names - - all these attributes contain all found modules even if are not enabled + - `modules` - list of available attributes + - `modules_by_id` - dictionary of modules mapped by their ids + - `modules_by_name` - dictionary of modules mapped by their names + - all these attributes contain all found modules even if are not enabled - helper methods - - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them - - `collect_plugin_paths` collect plugin paths from all enabled modules - - output is always dictionary with all keys and values as list - ``` - { - "publish": [], - "create": [], - "load": [], - "actions": [] - } - ``` + - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them + - `collect_plugin_paths` collects plugin paths from all enabled modules + - output is always dictionary with all keys and values as an list + ``` + { + "publish": [], + "create": [], + "load": [], + "actions": [] + } + ``` ### TrayModulesManager -- inherit from `ModulesManager` -- has specific implementations for Pype Tray tool and handle `ITrayModule` methods +- inherits from `ModulesManager` +- has specific implementation for Pype Tray tool and handle `ITrayModule` methods \ No newline at end of file From a7932ff6d536fb00415158b617d59a348afaa455 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:38:15 +0200 Subject: [PATCH 101/115] Fix typos --- .../modules/example_addons/example_addon/addon.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 5a25b80616..5573e33cc1 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -21,11 +21,11 @@ from openpype_interfaces import ( ) -# Settings definiton of this addon using `JsonFilesSettingsDef` -# - JsonFilesSettingsDef is prepared settings definiton using json files -# to define settings and store defaul values +# Settings definition of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definition using json files +# to define settings and store default values class AddonSettingsDef(JsonFilesSettingsDef): - # This will add prefix to every schema and template from `schemas` + # This will add prefixes to every schema and template from `schemas` # subfolder. # - it is not required to fill the prefix but it is highly # recommended as schemas and templates may have name clashes across @@ -48,7 +48,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """This Addon has defined it's settings and interface. - This example has system settings with enabled option. And use + This example has system settings with an enabled option. And use few other interfaces: - `IPluginPaths` to define custom plugin paths - `ITrayAction` to be shown in tray tool @@ -70,7 +70,7 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): def tray_init(self): """Implementation of abstract method for `ITrayAction`. - We're definetely in trat tool so we can precreate dialog. + We're definitely in tray tool so we can pre create dialog. """ self._create_dialog() @@ -101,7 +101,7 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """Show dialog with connected modules. This can be called from anywhere but can also crash in headless mode. - There is not way how to prevent addon to do invalid operations if he's + There is no way to prevent addon to do invalid operations if he's not handling them. """ # Make sure dialog is created From 3643b8e1bd7936dd840b343f9b70946600e249d7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:43:43 +0200 Subject: [PATCH 102/115] Hound --- .../modules/default_modules/sync_server/providers/gdrive.py | 3 +-- .../default_modules/sync_server/providers/local_drive.py | 3 +-- .../default_modules/sync_server/sync_server_module.py | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 5db728f2de..da54eecb8e 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -122,8 +122,7 @@ class GDriveHandler(AbstractProvider): { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index b3482ac1d8..9678d38ed8 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -56,8 +56,7 @@ class LocalDriveHandler(AbstractProvider): { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 39b5c9314e..976a349bfa 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -445,7 +445,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # # Methods for Settings to get appriate values to fill forms # def get_configurable_items(self, scope=None): # """ - # Returns list of sites that could be configurable for all projects. + # Returns list of sites that could be configurable for all projects # # Could be filtered by 'scope' argument (list) # @@ -468,8 +468,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # }, # { # key:"credentials_url", label:"Credentials url", - # "value":"'c:/projects/cred.json'", "type": "text", - # "namespace": "{project_setting}/global/sync_server/ + # "value":"'c:/projects/cred.json'", "type": "text", # noqa: E501 + # "namespace": "{project_setting}/global/sync_server/ # noqa: E501 # sites" # } # ] From 2230d60ff407f033ab7127d5f03c3b62e0309e80 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:48:28 +0200 Subject: [PATCH 103/115] #1976 - added 'key' --- .../default_modules/sync_server/providers/gdrive.py | 3 +++ .../sync_server/providers/local_drive.py | 13 +++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index da54eecb8e..3bfd6f4854 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -120,6 +120,7 @@ class GDriveHandler(AbstractProvider): editable = [ # credentials could be override on Project or User level { + 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 @@ -127,6 +128,7 @@ class GDriveHandler(AbstractProvider): # roots could be override only on Project leve, User cannot # { + 'key': "roots", 'label': "Roots", 'type': 'dict' } @@ -145,6 +147,7 @@ class GDriveHandler(AbstractProvider): editable = [ # credentials could be override on Project or User level { + 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 9678d38ed8..4b703267d5 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -49,18 +49,10 @@ class LocalDriveHandler(AbstractProvider): Returns: (list) of dict """ - # {platform} tells that value is multiplatform and only specific OS - # should be returned + # for non 'studio' sites, 'studio' is configured in Anatomy editable = [ - # credentials could be override on Project or User level - { - 'label': "Credentials url", - 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 - }, - # roots could be override only on Project leve, User cannot - # { + 'key': "roots", 'label': "Roots", 'type': 'dict' } @@ -78,6 +70,7 @@ class LocalDriveHandler(AbstractProvider): """ editable = [ { + 'key': "roots", 'label': "Roots", 'type': 'dict' } From fc0872a99750e0c648f7b5c77bb1de65c753a924 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:52:25 +0200 Subject: [PATCH 104/115] #1976 - small fixes --- .../modules/default_modules/sync_server/providers/gdrive.py | 5 ++--- .../default_modules/sync_server/providers/local_drive.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 3bfd6f4854..8c93f41d67 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -8,7 +8,7 @@ import platform from openpype.api import Logger from openpype.api import get_system_settings from .abstract_provider import AbstractProvider -from ..utils import time_function, ResumableError, EditableScopes +from ..utils import time_function, ResumableError log = Logger().get_logger("SyncServer") @@ -122,8 +122,7 @@ class GDriveHandler(AbstractProvider): { 'key': "credentials_url", 'label': "Credentials url", - 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 + 'type': 'text' }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 4b703267d5..8e5f170bc9 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -7,8 +7,6 @@ import time from openpype.api import Logger, Anatomy from .abstract_provider import AbstractProvider -from ..utils import EditableScopes - log = Logger().get_logger("SyncServer") From a5bbe779fbec6fb061234072017aeb212630a594 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:56:43 +0200 Subject: [PATCH 105/115] Hound --- .../default_modules/sync_server/providers/gdrive.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 8c93f41d67..f1ec0b6a0d 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -118,14 +118,13 @@ class GDriveHandler(AbstractProvider): # {platform} tells that value is multiplatform and only specific OS # should be returned editable = [ - # credentials could be override on Project or User level - { + # credentials could be overriden on Project or User level + { 'key': "credentials_url", 'label': "Credentials url", 'type': 'text' }, - # roots could be override only on Project leve, User cannot - # + # roots could be overriden only on Project leve, User cannot { 'key': "roots", 'label': "Roots", @@ -149,8 +148,7 @@ class GDriveHandler(AbstractProvider): 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 } ] return editable From 72b2f44fa9e829925858e70d621d8622f60d1b5b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:25:58 +0200 Subject: [PATCH 106/115] added loading of steps from schema --- openpype/settings/entities/input_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index ebc70b840d..128625619a 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,6 +379,10 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) + steps = self.schema_data.get("steps", None) + if steps is None: + steps = 1 / (10 ** self.decimal) + self.steps = steps def _convert_to_valid_type(self, value): if isinstance(value, str): From 7e973a5de1fba402a04d1e780a4a6a35e78c8afc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:27:32 +0200 Subject: [PATCH 107/115] added steps to Number widget --- openpype/tools/settings/settings/widgets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index b821c3bb2c..2caf8c33ba 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -92,11 +92,15 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox): min_value = kwargs.pop("minimum", -99999) max_value = kwargs.pop("maximum", 99999) decimals = kwargs.pop("decimal", 0) + steps = kwargs.pop("steps", None) + super(NumberSpinBox, self).__init__(*args, **kwargs) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setDecimals(decimals) self.setMinimum(min_value) self.setMaximum(max_value) + if steps is not None: + self.setSingleStep(steps) def focusInEvent(self, event): super(NumberSpinBox, self).focusInEvent(event) From 7af864a6e0eeb256177525785707d66cc385495c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:28:17 +0200 Subject: [PATCH 108/115] steps can not be set --- openpype/settings/entities/input_entities.py | 5 +---- openpype/tools/settings/settings/item_widgets.py | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 128625619a..4afa0d9484 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,10 +379,7 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) - steps = self.schema_data.get("steps", None) - if steps is None: - steps = 1 / (10 ** self.decimal) - self.steps = steps + self.steps = self.schema_data.get("steps", None) def _convert_to_valid_type(self, value): if isinstance(value, str): diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 736ba77652..da74c2adc5 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -411,7 +411,8 @@ class NumberWidget(InputWidget): kwargs = { "minimum": self.entity.minimum, "maximum": self.entity.maximum, - "decimal": self.entity.decimal + "decimal": self.entity.decimal, + "steps": self.entity.steps } self.input_field = NumberSpinBox(self.content_widget, **kwargs) input_field_stretch = 1 @@ -426,6 +427,10 @@ class NumberWidget(InputWidget): int(self.entity.minimum * slider_multiplier), int(self.entity.maximum * slider_multiplier) ) + if self.entity.steps is not None: + slider_widget.setSingleStep( + self.entity.steps * slider_multiplier + ) self.content_layout.addWidget(slider_widget, 1) From e3b8e25a1a15cbdc24519e49f0067126bb071610 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:33:09 +0200 Subject: [PATCH 109/115] added steps to readme --- openpype/settings/entities/schemas/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 9b53e89dd7..c8432f0f2e 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -316,6 +316,7 @@ How output of the schema could look like on save: - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +- key `"steps"` will change single step value of UI inputs (using arrows and wheel scroll) - for UI it is possible to show slider to enable this option set `show_slider` to `true` ``` { From e75e9a6465c59751ffb62ac143532255eef9a837 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:33:19 +0200 Subject: [PATCH 110/115] make sure that steps are not `0` --- openpype/settings/entities/input_entities.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 4afa0d9484..0ded3ab7e5 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,7 +379,11 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) - self.steps = self.schema_data.get("steps", None) + steps = self.schema_data.get("steps", None) + # Make sure that steps are not set to `0` + if steps == 0: + steps = None + self.steps = steps def _convert_to_valid_type(self, value): if isinstance(value, str): From 40a6712384e5fec86bd0ab1ccdae2ec8d8317fad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:35:19 +0200 Subject: [PATCH 111/115] added steps to avalon mongo timeout --- .../entities/schemas/system_schema/schema_modules.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..b52a646954 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -28,7 +28,8 @@ "type": "number", "key": "AVALON_TIMEOUT", "minimum": 0, - "label": "Avalon Mongo Timeout (ms)" + "label": "Avalon Mongo Timeout (ms)", + "steps": 100 }, { "type": "path", From d961e0a26209fa13da5c184d68be765bfca7c956 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 15:17:23 +0200 Subject: [PATCH 112/115] replaced `providers-enum` with `sync-server-providers` to be able set system settings for provider --- openpype/settings/entities/__init__.py | 8 ++-- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ openpype/settings/entities/enum_entity.py | 38 --------------- .../schemas/system_schema/schema_modules.json | 9 +--- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 8c30d5044c..aae2d1fa89 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -105,7 +105,6 @@ from .enum_entity import ( AppsEnumEntity, ToolsEnumEntity, TaskTypeEnumEntity, - ProvidersEnum, DeadlineUrlEnumEntity, AnatomyTemplatesEnumEntity ) @@ -113,7 +112,10 @@ from .enum_entity import ( from .list_entity import ListEntity from .dict_immutable_keys_entity import DictImmutableKeysEntity from .dict_mutable_keys_entity import DictMutableKeysEntity -from .dict_conditional import DictConditionalEntity +from .dict_conditional import ( + DictConditionalEntity, + SyncServerProviders +) from .anatomy_entities import AnatomyEntity @@ -161,7 +163,6 @@ __all__ = ( "AppsEnumEntity", "ToolsEnumEntity", "TaskTypeEnumEntity", - "ProvidersEnum", "DeadlineUrlEnumEntity", "AnatomyTemplatesEnumEntity", @@ -172,6 +173,7 @@ __all__ = ( "DictMutableKeysEntity", "DictConditionalEntity", + "SyncServerProviders", "AnatomyEntity" ) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d7b416921c..6f27760570 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -724,3 +724,49 @@ class DictConditionalEntity(ItemEntity): for children in self.children.values(): for child_entity in children: child_entity.reset_callbacks() + + +class SyncServerProviders(DictConditionalEntity): + schema_types = ["sync-server-providers"] + + def _add_children(self): + self.enum_key = "provider" + self.enum_label = "Provider" + + enum_children = self._get_enum_children() + if not enum_children: + enum_children.append({ + "key": None, + "label": "< Nothing >" + }) + self.enum_children = enum_children + + super(SyncServerProviders, self)._add_children() + + def _get_enum_children(self): + from openpype_modules import sync_server + + from openpype_modules.sync_server.providers import lib as lib_providers + + provider_code_to_label = {} + providers = lib_providers.factory.providers + for provider_code, provider_info in providers.items(): + provider, _ = provider_info + provider_code_to_label[provider_code] = provider.LABEL + + system_settings_schema = ( + sync_server + .SyncServerModule + .get_system_settings_schema() + ) + + enum_children = [] + for provider_code, configurables in system_settings_schema.items(): + label = provider_code_to_label.get(provider_code) or provider_code + + enum_children.append({ + "key": provider_code, + "label": label, + "children": configurables + }) + return enum_children diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index cb532c5ae0..66279f529d 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -407,44 +407,6 @@ class TaskTypeEnumEntity(BaseEnumEntity): self._current_value = new_value -class ProvidersEnum(BaseEnumEntity): - schema_types = ["providers-enum"] - - def _item_initalization(self): - self.multiselection = False - self.value_on_not_set = "" - self.enum_items = [] - self.valid_keys = set() - self.valid_value_types = (str, ) - self.placeholder = None - - def _get_enum_values(self): - from openpype_modules.sync_server.providers import lib as lib_providers - - providers = lib_providers.factory.providers - - valid_keys = set() - valid_keys.add('') - enum_items = [{'': 'Choose Provider'}] - for provider_code, provider_info in providers.items(): - provider, _ = provider_info - enum_items.append({provider_code: provider.LABEL}) - valid_keys.add(provider_code) - - return enum_items, valid_keys - - def set_override_state(self, *args, **kwargs): - super(ProvidersEnum, self).set_override_state(*args, **kwargs) - - self.enum_items, self.valid_keys = self._get_enum_values() - - value_on_not_set = list(self.valid_keys)[0] - if self._current_value is NOT_SET: - self._current_value = value_on_not_set - - self.value_on_not_set = value_on_not_set - - class DeadlineUrlEnumEntity(BaseEnumEntity): schema_types = ["deadline_url-enum"] diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..9961341ba5 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -121,14 +121,7 @@ "collapsible_key": false, "object_type": { - "type": "dict", - "children": [ - { - "type": "providers-enum", - "key": "provider", - "label": "Provider" - } - ] + "type": "sync-server-providers" } } ] From b2b248f818f5e1d2cd411fd14cf3e43d23875cda Mon Sep 17 00:00:00 2001 From: "felix.wang" Date: Fri, 10 Sep 2021 14:43:09 -0700 Subject: [PATCH 113/115] addressing comments PR#1996 --- openpype/api.py | 4 +- openpype/lib/__init__.py | 6 ++- openpype/lib/path_tools.py | 40 +++++++++++++++++-- .../action_create_project_structure.py | 2 +- openpype/settings/__init__.py | 4 +- openpype/settings/lib.py | 29 -------------- 6 files changed, 44 insertions(+), 41 deletions(-) diff --git a/openpype/api.py b/openpype/api.py index dcff127e9f..e4bbb104a3 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -4,7 +4,6 @@ from .settings import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_project_basic_paths, SystemSettings, ProjectSettings @@ -26,7 +25,8 @@ from .lib import ( get_global_environments, get_local_site_id, change_openpype_mongo_url, - create_project_folders + create_project_folders, + get_project_basic_paths ) from .lib.mongo import ( diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 4abfd69175..9bc68c9558 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -144,7 +144,8 @@ from .path_tools import ( version_up, get_version_from_path, get_last_version_from_path, - create_project_folders + create_project_folders, + get_project_basic_paths ) from .editorial import ( @@ -278,5 +279,6 @@ __all__ = [ "frames_to_secons", "frames_to_timecode", "make_sequence_collection", - "create_project_folders" + "create_project_folders", + "get_project_basic_paths" ] diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index fab0879759..42b5db9e25 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -1,13 +1,14 @@ +import json +import logging import os import re -import logging -from openpype.api import Anatomy + +from .anatomy import Anatomy +from openpype.settings import get_project_settings log = logging.getLogger(__name__) -pattern_array = re.compile(r"\[.*\]") -project_root_key = "__project_root__" def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" @@ -126,6 +127,8 @@ def get_last_version_from_path(path_dir, filter): def compute_paths(basic_paths_items, project_root): + pattern_array = re.compile(r"\[.*\]") + project_root_key = "__project_root__" output = [] for path_items in basic_paths_items: clean_items = [] @@ -162,3 +165,32 @@ def create_project_folders(basic_paths, project_name): else: log.debug("Creating folder: {}".format(full_path)) os.makedirs(full_path) + + +def _list_path_items(folder_structure): + output = [] + for key, value in folder_structure.items(): + if not value: + output.append(key) + else: + paths = _list_path_items(value) + for path in paths: + if not isinstance(path, (list, tuple)): + path = [path] + + output.append([key, *path]) + + return output + + +def get_project_basic_paths(project_name): + project_settings = get_project_settings(project_name) + folder_structure = ( + project_settings["global"]["project_folder_structure"] + ) + if not folder_structure: + return [] + + if isinstance(folder_structure, str): + folder_structure = json.loads(folder_structure) + return _list_path_items(folder_structure) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py index b0de792473..94f359c317 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py @@ -2,7 +2,7 @@ import os import re import json -from openpype.modules.ftrack.lib import BaseAction, statics_icon +from openpype_modules.ftrack.lib import BaseAction, statics_icon from openpype.api import get_project_basic_paths, create_project_folders diff --git a/openpype/settings/__init__.py b/openpype/settings/__init__.py index 0d6be51253..74f2684b2a 100644 --- a/openpype/settings/__init__.py +++ b/openpype/settings/__init__.py @@ -21,8 +21,7 @@ from .lib import ( get_current_project_settings, get_anatomy_settings, get_environments, - get_local_settings, - get_project_basic_paths + get_local_settings ) from .entities import ( SystemSettings, @@ -52,7 +51,6 @@ __all__ = ( "get_anatomy_settings", "get_environments", "get_local_settings", - "get_project_basic_paths", "SystemSettings", "ProjectSettings" ) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 749b337df7..60ed54bd4a 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -941,35 +941,6 @@ def get_general_environments(): return environments -def _list_path_items(folder_structure): - output = [] - for key, value in folder_structure.items(): - if not value: - output.append(key) - else: - paths = _list_path_items(value) - for path in paths: - if not isinstance(path, (list, tuple)): - path = [path] - - output.append([key, *path]) - - return output - - -def get_project_basic_paths(project_name): - project_settings = get_project_settings(project_name) - folder_structure = ( - project_settings["global"]["project_folder_structure"] - ) - if not folder_structure: - return [] - - if isinstance(folder_structure, str): - folder_structure = json.loads(folder_structure) - return _list_path_items(folder_structure) - - def clear_metadata_from_settings(values): """Remove all metadata keys from loaded settings.""" if isinstance(values, dict): From a0338cb548b94fccd9ed0d1b799f10d80eb4a9d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 13 Sep 2021 10:40:29 +0200 Subject: [PATCH 114/115] avalon-core update --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index f48fce09c0..b3e4959778 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit f48fce09c0986c1fd7f6731de33907be46b436c5 +Subproject commit b3e49597786c931c13bca207769727d5fc56d5f6 From 0cb2cbb14f39a52befde9a6e7d7f4635594618fc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 13 Sep 2021 12:27:31 +0200 Subject: [PATCH 115/115] Update openpype/modules/README.md --- openpype/modules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index abc7ed3961..5716324365 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -9,7 +9,7 @@ OpenPype modules should contain separated logic of specific kind of implementati ### TODOs - add module/addon manifest - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) - - defying that folder is content of a module or an addon + - defining a folder as a content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module