From 3e87997b401da0ba7e57ab0707b78ada9168ff2c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 9 Aug 2021 10:54:05 +0200 Subject: [PATCH 01/98] 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 02/98] 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 03/98] 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 04/98] 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 05/98] 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 06/98] 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 07/98] 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 08/98] 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 09/98] 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 10/98] 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 11/98] 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 12/98] 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 13/98] 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 14/98] 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 15/98] 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 16/98] 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 17/98] 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 18/98] 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 19/98] 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 20/98] 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 21/98] 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 22/98] 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 23/98] 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 24/98] 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 25/98] 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 26/98] 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 27/98] 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 28/98] 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 29/98] 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 30/98] 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 31/98] 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 32/98] 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 33/98] 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 34/98] 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 35/98] 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 36/98] 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 37/98] 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 38/98] 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 39/98] 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 40/98] 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 41/98] 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 42/98] 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 43/98] 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 44/98] 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 45/98] 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 46/98] 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 47/98] 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 48/98] 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 49/98] 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 50/98] 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 51/98] 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 52/98] 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 53/98] 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 54/98] 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 55/98] 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 56/98] 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 57/98] 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 58/98] =?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 59/98] 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 60/98] 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 4ec7e18aad0044164fd56688dc70183205401866 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 2 Sep 2021 12:25:19 +0200 Subject: [PATCH 61/98] 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 62/98] 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 63/98] 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 64/98] 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 65/98] 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 66/98] 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 d4a6db63f467c740d13af5fc644b6ee6ab669d0c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 6 Sep 2021 11:41:26 +0200 Subject: [PATCH 67/98] 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 53c6c9818454cb850751a58b8a6eec3907e075c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 12:08:49 +0200 Subject: [PATCH 68/98] #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 c6d781d7df7ab6e847328fc8188d77e29fedc941 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 13:34:56 +0200 Subject: [PATCH 69/98] #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 70/98] 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 71/98] #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 72/98] #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 73/98] #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 74/98] #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 75/98] 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 76/98] 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 77/98] 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 78/98] 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 79/98] 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 80/98] 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 81/98] 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 82/98] 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 83/98] 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 84/98] 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 85/98] 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 3643b8e1bd7936dd840b343f9b70946600e249d7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:43:43 +0200 Subject: [PATCH 86/98] 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 87/98] #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 88/98] #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 89/98] 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 90/98] 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 91/98] 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 92/98] 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 93/98] 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 94/98] 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 95/98] 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 96/98] 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 97/98] 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 98/98] 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