From 3bbd29dafc262d8984e83e4cf638e6a8b8a033e2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 6 Oct 2024 15:14:22 +0200 Subject: [PATCH 001/100] Show/hide attributes per instance based on status of other toggles --- .../extract_usd_layer_contributions.py | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index acdc5276f7..a67c6ec702 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -458,7 +458,22 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, return new_instance @classmethod - def get_attribute_defs(cls): + def get_attr_defs_for_instance(cls, create_context, instance): + # Filtering of instance, if needed, can be customized + if not cls.instance_matches_plugin_families(instance): + return [] + + # Attributes logic + disabled = False + publish_attributes = instance["publish_attributes"].get( + cls.__name__, {}) + + enabled = publish_attributes.get("contribution_enabled", True) + variant_enabled = enabled and publish_attributes.get( + "contribution_apply_as_variant", True) + + disabled = not enabled + variant_disabled = not variant_enabled return [ UISeparatorDef("usd_container_settings1"), @@ -484,7 +499,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "the contribution itself will be added to the " "department layer." ), - default="usdAsset"), + default="usdAsset", + hidden=disabled), EnumDef("contribution_target_product_init", label="Initialize as", tooltip=( @@ -495,7 +511,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "setting will do nothing." ), items=["asset", "shot"], - default="asset"), + default="asset", + hidden=disabled), # Asset layer, e.g. model.usd, look.usd, rig.usd EnumDef("contribution_layer", @@ -507,7 +524,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "the list) will contribute as a stronger opinion." ), items=list(cls.contribution_layers.keys()), - default="model"), + default="model", + hidden=disabled), BoolDef("contribution_apply_as_variant", label="Add as variant", tooltip=( @@ -518,13 +536,16 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "appended to as a sublayer to the department layer " "instead." ), - default=True), + default=True, + hidden=disabled), TextDef("contribution_variant_set_name", label="Variant Set Name", - default="{layer}"), + default="{layer}", + hidden=variant_disabled), TextDef("contribution_variant", label="Variant Name", - default="{variant}"), + default="{variant}", + hidden=variant_disabled), BoolDef("contribution_variant_is_default", label="Set as default variant selection", tooltip=( @@ -535,10 +556,41 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "The behavior is unpredictable if multiple instances " "for the same variant set have this enabled." ), - default=False), + default=False, + hidden=variant_disabled), UISeparatorDef("usd_container_settings3"), ] + @classmethod + def register_create_context_callbacks(cls, create_context): + create_context.add_value_changed_callback(cls.on_values_changed) + + @classmethod + def on_values_changed(cls, event): + """Update instance attribute definitions on attribute changes.""" + + # Update attributes if any of the following plug-in attributes + # change: + keys = ["contribution_enabled", "contribution_apply_as_variant"] + + for instance_change in event["changes"]: + instance = instance_change["instance"] + if not cls.instance_matches_plugin_families(instance): + continue + value_changes = instance_change["changes"] + plugin_attribute_changes = ( + value_changes.get("publish_attributes", {}) + .get(cls.__name__, {})) + + if not any(key in plugin_attribute_changes for key in keys): + continue + + # Update the attribute definitions + new_attrs = cls.get_attr_defs_for_instance( + event["create_context"], instance + ) + instance.set_publish_plugin_attr_defs(cls.__name__, new_attrs) + class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions): """ @@ -551,9 +603,8 @@ class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions): label = CollectUSDLayerContributions.label + " (Look)" @classmethod - def get_attribute_defs(cls): - defs = super(CollectUSDLayerContributionsHoudiniLook, - cls).get_attribute_defs() + def get_attr_defs_for_instance(cls, create_context, instance): + defs = super().get_attr_defs_for_instance(create_context, instance) # Update default for department layer to look layer_def = next(d for d in defs if d.key == "contribution_layer") From 202800a48749e74f9234ac86d2a4778e170ea33e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:35:11 +0200 Subject: [PATCH 002/100] added helper function to get task type for an instance --- client/ayon_core/pipeline/create/context.py | 58 +++++++++++++++++---- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index adbb03a820..6ec2ea284c 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -254,7 +254,7 @@ class CreateContext: # Context validation cache self._folder_id_by_folder_path = {} - self._task_names_by_folder_path = {} + self._task_infos_by_folder_path = {} self.thumbnail_paths_by_instance_id = {} @@ -567,7 +567,7 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} self._folder_id_by_folder_path = {} - self._task_names_by_folder_path = {} + self._task_infos_by_folder_path = {} self._event_hub.clear_callbacks() def reset_finalization(self): @@ -1468,6 +1468,42 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) + def get_instances_task_type( + self, instances: Optional[Iterable["CreatedInstance"]] = None + ) -> Dict[str, Optional[str]]: + """Helper function to get task type of task on instance. + + Based on context set on instance (using 'folderPath' and 'task') tries + to find task type. + + Task type is 'None' if task is not set or is not valid, and + is always 'None' for instances with promised context. + + Args: + instances (Optional[Iterable[CreatedInstance]]): Instances to + get task type. If not provided all instances are used. + + Returns: + Dict[str, Optional[str]]: Task type by instance id. + + """ + context_infos = self.get_instances_context_info(instances) + output = {} + for instance_id, context_info in context_infos.items(): + folder_path = context_info.folder_path + task_name = context_info.task_name + if not task_name or not folder_path: + output[instance_id] = None + continue + task_info = ( + self._task_infos_by_folder_path.get(folder_path) or {} + ).get(task_name) + task_type = None + if task_info is not None: + task_type = task_info["task_type"] + output[instance_id] = task_type + return output + def get_instances_context_info( self, instances: Optional[Iterable["CreatedInstance"]] = None ) -> Dict[str, InstanceContextInfo]: @@ -1508,6 +1544,7 @@ class CreateContext: if instance.has_promised_context: context_info.folder_is_valid = True context_info.task_is_valid = True + # NOTE missing task type continue # TODO allow context promise folder_path = context_info.folder_path @@ -1522,7 +1559,7 @@ class CreateContext: task_name = context_info.task_name if task_name is not None: - tasks_cache = self._task_names_by_folder_path.get(folder_path) + tasks_cache = self._task_infos_by_folder_path.get(folder_path) if tasks_cache is not None: context_info.task_is_valid = task_name in tasks_cache continue @@ -1574,15 +1611,17 @@ class CreateContext: tasks_entities = ayon_api.get_tasks( project_name, folder_ids=folder_paths_by_id.keys(), - fields={"name", "folderId"} + fields={"name", "folderId", "taskType"} ) - task_names_by_folder_path = collections.defaultdict(set) + task_infos_by_folder_path = collections.defaultdict(dict) for task_entity in tasks_entities: folder_id = task_entity["folderId"] folder_path = folder_paths_by_id[folder_id] - task_names_by_folder_path[folder_path].add(task_entity["name"]) - self._task_names_by_folder_path.update(task_names_by_folder_path) + task_infos_by_folder_path[folder_path][task_entity["name"]] = { + "task_type": task_entity["taskType"], + } + self._task_infos_by_folder_path.update(task_infos_by_folder_path) for instance in to_validate: folder_path = instance["folderPath"] @@ -1593,15 +1632,16 @@ class CreateContext: folder_path = folder_entities[0]["path"] instance["folderPath"] = folder_path - if folder_path not in task_names_by_folder_path: + if folder_path not in task_infos_by_folder_path: continue context_info = info_by_instance_id[instance.id] context_info.folder_is_valid = True if ( not task_name - or task_name in task_names_by_folder_path[folder_path] + or task_name in task_infos_by_folder_path[folder_path] ): + task_info = task_infos_by_folder_path[folder_path] context_info.task_is_valid = True return info_by_instance_id From d0ed7f086e05a7bf5633b04ebb23b81ec5659e11 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:41:29 +0200 Subject: [PATCH 003/100] small improvement --- client/ayon_core/pipeline/create/context.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 6ec2ea284c..f8e277dccf 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1490,17 +1490,19 @@ class CreateContext: context_infos = self.get_instances_context_info(instances) output = {} for instance_id, context_info in context_infos.items(): - folder_path = context_info.folder_path task_name = context_info.task_name - if not task_name or not folder_path: + if not task_name or not context_info.is_valid: output[instance_id] = None continue - task_info = ( - self._task_infos_by_folder_path.get(folder_path) or {} - ).get(task_name) + task_type = None - if task_info is not None: - task_type = task_info["task_type"] + tasks_cache = self._task_infos_by_folder_path.get( + context_info.folder_path + ) + if tasks_cache is not None: + task_info = tasks_cache.get(task_name) + if task_info is not None: + task_type = task_info["task_type"] output[instance_id] = task_type return output From 385e5bd02c551d8d0c340dbab9d50d3ba3f18047 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:46:44 +0200 Subject: [PATCH 004/100] change name of method --- client/ayon_core/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f8e277dccf..36bef9f88b 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1468,7 +1468,7 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) - def get_instances_task_type( + def get_instances_task_types( self, instances: Optional[Iterable["CreatedInstance"]] = None ) -> Dict[str, Optional[str]]: """Helper function to get task type of task on instance. From 1ce9bcf9d2ea3a6235ef8b1737e09b749966c302 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:21:05 +0200 Subject: [PATCH 005/100] revert some changes --- client/ayon_core/pipeline/create/context.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 36bef9f88b..ac3851a51a 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -254,7 +254,7 @@ class CreateContext: # Context validation cache self._folder_id_by_folder_path = {} - self._task_infos_by_folder_path = {} + self._task_names_by_folder_path = {} self.thumbnail_paths_by_instance_id = {} @@ -567,7 +567,7 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} self._folder_id_by_folder_path = {} - self._task_infos_by_folder_path = {} + self._task_names_by_folder_path = {} self._event_hub.clear_callbacks() def reset_finalization(self): @@ -1561,7 +1561,7 @@ class CreateContext: task_name = context_info.task_name if task_name is not None: - tasks_cache = self._task_infos_by_folder_path.get(folder_path) + tasks_cache = self._task_names_by_folder_path.get(folder_path) if tasks_cache is not None: context_info.task_is_valid = task_name in tasks_cache continue @@ -1613,17 +1613,16 @@ class CreateContext: tasks_entities = ayon_api.get_tasks( project_name, folder_ids=folder_paths_by_id.keys(), - fields={"name", "folderId", "taskType"} + fields={"name", "folderId"} ) - task_infos_by_folder_path = collections.defaultdict(dict) + task_names_by_folder_path = collections.defaultdict(set) for task_entity in tasks_entities: folder_id = task_entity["folderId"] folder_path = folder_paths_by_id[folder_id] - task_infos_by_folder_path[folder_path][task_entity["name"]] = { - "task_type": task_entity["taskType"], - } - self._task_infos_by_folder_path.update(task_infos_by_folder_path) + task_names_by_folder_path[folder_path].add(task_entity["name"]) + + self._task_names_by_folder_path.update(task_names_by_folder_path) for instance in to_validate: folder_path = instance["folderPath"] From 360a8b39b078714a32255a9d27925bd2f57c54ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:37:47 +0200 Subject: [PATCH 006/100] implemented logic to cache entities --- client/ayon_core/pipeline/create/context.py | 422 +++++++++++++++----- 1 file changed, 324 insertions(+), 98 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index ac3851a51a..7c9449cb21 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -12,6 +12,7 @@ from typing import ( Iterable, Tuple, List, + Set, Dict, Any, Callable, @@ -252,8 +253,11 @@ class CreateContext: # Shared data across creators during collection phase self._collection_shared_data = None - # Context validation cache + # Entities cache + self._folder_entities_by_id = {} + self._task_entities_by_id = {} self._folder_id_by_folder_path = {} + self._task_ids_by_folder_path = {} self._task_names_by_folder_path = {} self.thumbnail_paths_by_instance_id = {} @@ -356,12 +360,12 @@ class CreateContext: return self._host_is_valid @property - def host_name(self): + def host_name(self) -> str: if hasattr(self.host, "name"): return self.host.name return os.environ["AYON_HOST_NAME"] - def get_current_project_name(self): + def get_current_project_name(self) -> Optional[str]: """Project name which was used as current context on context reset. Returns: @@ -370,7 +374,7 @@ class CreateContext: return self._current_project_name - def get_current_folder_path(self): + def get_current_folder_path(self) -> Optional[str]: """Folder path which was used as current context on context reset. Returns: @@ -379,7 +383,7 @@ class CreateContext: return self._current_folder_path - def get_current_task_name(self): + def get_current_task_name(self) -> Optional[str]: """Task name which was used as current context on context reset. Returns: @@ -388,7 +392,7 @@ class CreateContext: return self._current_task_name - def get_current_task_type(self): + def get_current_task_type(self) -> Optional[str]: """Task type which was used as current context on context reset. Returns: @@ -403,7 +407,7 @@ class CreateContext: self._current_task_type = task_type return self._current_task_type - def get_current_project_entity(self): + def get_current_project_entity(self) -> Optional[Dict[str, Any]]: """Project entity for current context project. Returns: @@ -419,26 +423,21 @@ class CreateContext: self._current_project_entity = project_entity return copy.deepcopy(self._current_project_entity) - def get_current_folder_entity(self): + def get_current_folder_entity(self) -> Optional[Dict[str, Any]]: """Folder entity for current context folder. Returns: - Union[dict[str, Any], None]: Folder entity. + Optional[dict[str, Any]]: Folder entity. """ if self._current_folder_entity is not _NOT_SET: return copy.deepcopy(self._current_folder_entity) - folder_entity = None + folder_path = self.get_current_folder_path() - if folder_path: - project_name = self.get_current_project_name() - folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path - ) - self._current_folder_entity = folder_entity + self._current_folder_entity = self.get_folder_entity(folder_path) return copy.deepcopy(self._current_folder_entity) - def get_current_task_entity(self): + def get_current_task_entity(self) -> Optional[Dict[str, Any]]: """Task entity for current context task. Returns: @@ -447,18 +446,12 @@ class CreateContext: """ if self._current_task_entity is not _NOT_SET: return copy.deepcopy(self._current_task_entity) - task_entity = None + + folder_path = self.get_current_folder_path() task_name = self.get_current_task_name() - if task_name: - folder_entity = self.get_current_folder_entity() - if folder_entity: - project_name = self.get_current_project_name() - task_entity = ayon_api.get_task_by_name( - project_name, - folder_id=folder_entity["id"], - task_name=task_name - ) - self._current_task_entity = task_entity + self._current_task_entity = self.get_task_entity( + folder_path, task_name + ) return copy.deepcopy(self._current_task_entity) def get_current_workfile_path(self): @@ -566,8 +559,14 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} + + self._folder_entities_by_id = {} + self._task_entities_by_id = {} + self._folder_id_by_folder_path = {} + self._task_ids_by_folder_path = {} self._task_names_by_folder_path = {} + self._event_hub.clear_callbacks() def reset_finalization(self): @@ -1468,42 +1467,265 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) - def get_instances_task_types( + def get_folder_entities(self, folder_paths: Iterable[str]): + """Get folder entities by paths. + + Args: + folder_paths (Iterable[str]): Folder paths. + + Returns: + Dict[str, Optional[Dict[str, Any]]]: Folder entities by path. + + """ + output = { + folder_path: None + for folder_path in folder_paths + if folder_path is not None + } + remainders = set() + for folder_path in output: + # Skip empty/invalid folder paths + if folder_path is None or "/" not in folder_path: + continue + + if folder_path not in self._folder_id_by_folder_path: + remainders.add(folder_path) + continue + + folder_id = self._folder_id_by_folder_path.get(folder_path) + if not folder_id: + output[folder_path] = None + continue + + folder_entity = self._folder_entities_by_id.get(folder_id) + if folder_entity: + output[folder_path] = folder_entity + else: + remainders.add(folder_path) + + if not remainders: + return output + + folder_paths_by_id = {} + for folder_entity in ayon_api.get_folders( + self.project_name, + folder_paths=remainders, + ): + folder_id = folder_entity["id"] + folder_path = folder_entity["path"] + folder_paths_by_id[folder_id] = folder_path + output[folder_path] = folder_entity + self._folder_entities_by_id[folder_id] = folder_entity + self._folder_id_by_folder_path[folder_path] = folder_id + + return output + + def get_task_entities( + self, + task_names_by_folder_paths: Dict[str, Set[str]] + ) -> Dict[str, Dict[str, Optional[Dict[str, Any]]]]: + """Get task entities by folder path and task name. + + Entities are cached until reset. + + Args: + task_names_by_folder_paths (Dict[str, Set[str]]): Task names by + folder path. + + Returns: + Dict[str, Dict[str, Dict[str, Any]]]: Task entities by folder path + and task name. + + """ + output = {} + for folder_path, task_names in task_names_by_folder_paths.items(): + if folder_path is None: + continue + output[folder_path] = { + task_name: None + for task_name in task_names + if task_name is not None + } + + missing_folder_paths = set() + for folder_path, output_task_entities_by_name in output.items(): + if not output_task_entities_by_name: + continue + + if folder_path not in self._task_ids_by_folder_path: + missing_folder_paths.add(folder_path) + continue + + all_tasks_filled = True + task_ids = self._task_ids_by_folder_path[folder_path] + task_entities_by_name = {} + for task_id in task_ids: + task_entity = self._task_entities_by_id.get(task_id) + if task_entity is None: + all_tasks_filled = False + continue + task_entities_by_name[task_entity["name"]] = task_entity + + any_missing = False + for task_name in set(output_task_entities_by_name): + task_entity = task_entities_by_name.get(task_name) + if task_entity is None: + any_missing = True + continue + + output_task_entities_by_name[task_name] = task_entity + + if any_missing and not all_tasks_filled: + missing_folder_paths.add(folder_path) + + if not missing_folder_paths: + return output + + folder_entities_by_path = self.get_folder_entities( + missing_folder_paths + ) + folder_ids = set() + for folder_path, folder_entity in folder_entities_by_path.items(): + if folder_entity is not None: + folder_ids.add(folder_entity["id"]) + + if not folder_ids: + return output + + task_entities_by_parent_id = collections.defaultdict(list) + for task_entity in ayon_api.get_tasks( + self.project_name, + folder_ids=folder_ids + ): + folder_id = task_entity["folderId"] + task_entities_by_parent_id[folder_id].append(task_entity) + + for folder_id, task_entities in task_entities_by_parent_id.items(): + folder_path = self._folder_id_by_folder_path.get(folder_id) + task_ids = set() + task_names = set() + for task_entity in task_entities: + task_id = task_entity["id"] + task_name = task_entity["name"] + task_ids.add(task_id) + task_names.add(task_name) + self._task_entities_by_id[task_id] = task_entity + + output[folder_path][task_name] = task_entity + self._task_ids_by_folder_path[folder_path] = task_ids + self._task_names_by_folder_path[folder_path] = task_names + + return output + + def get_folder_entity( + self, + folder_path: Optional[str], + ) -> Optional[Dict[str, Any]]: + """Get folder entity by path. + + Entities are cached until reset. + + Args: + folder_path (Optional[str]): Folder path. + + Returns: + Optional[Dict[str, Any]]: Folder entity. + + """ + if not folder_path: + return None + return self.get_folder_entities([folder_path]).get(folder_path) + + def get_task_entity( + self, + folder_path: Optional[str], + task_name: Optional[str], + ) -> Optional[Dict[str, Any]]: + """Get task entity by name and folder path. + + Entities are cached until reset. + + Args: + folder_path (Optional[str]): Folder path. + task_name (Optional[str]): Task name. + + Returns: + Optional[Dict[str, Any]]: Task entity. + + """ + if not folder_path or not task_name: + return None + + output = self.get_task_entities({folder_path: {task_name}}) + return output.get(folder_path, {}).get(task_name) + + def get_instances_folder_entities( self, instances: Optional[Iterable["CreatedInstance"]] = None - ) -> Dict[str, Optional[str]]: - """Helper function to get task type of task on instance. + ) -> Dict[str, Optional[Dict[str, Any]]]: + if instances is None: + instances = self._instances_by_id.values() + instances = list(instances) + output = { + instance.id: None + for instance in instances + } + if not instances: + return output - Based on context set on instance (using 'folderPath' and 'task') tries - to find task type. + folder_paths = { + instance.get("folderPath") + for instance in instances + } + folder_entities_by_path = self.get_folder_entities(folder_paths) + for instance in instances: + folder_path = instance.get("folderPath") + output[instance.id] = folder_entities_by_path.get(folder_path) + return output - Task type is 'None' if task is not set or is not valid, and - is always 'None' for instances with promised context. + def get_instances_task_entities( + self, instances: Optional[Iterable["CreatedInstance"]] = None + ): + """Get task entities for instances. Args: instances (Optional[Iterable[CreatedInstance]]): Instances to - get task type. If not provided all instances are used. + get task entities. If not provided all instances are used. Returns: - Dict[str, Optional[str]]: Task type by instance id. + Dict[str, Optional[Dict[str, Any]]]: Task entity by instance id. """ - context_infos = self.get_instances_context_info(instances) - output = {} - for instance_id, context_info in context_infos.items(): - task_name = context_info.task_name - if not task_name or not context_info.is_valid: - output[instance_id] = None - continue + if instances is None: + instances = self._instances_by_id.values() + instances = list(instances) - task_type = None - tasks_cache = self._task_infos_by_folder_path.get( - context_info.folder_path + output = { + instance.id: None + for instance in instances + } + if not instances: + return output + + filtered_instances = [] + task_names_by_folder_path = collections.defaultdict(set) + for instance in instances: + folder_path = instance.get("folderPath") + task_name = instance.get("task") + if not folder_path or not task_name: + continue + filtered_instances.append(instance) + task_names_by_folder_path[folder_path].add(task_name) + + task_entities_by_folder_path = self.get_task_entities( + task_names_by_folder_path + ) + for instance in filtered_instances: + folder_path = instance["folderPath"] + task_name = instance["task"] + output[instance.id] = ( + task_entities_by_folder_path[folder_path][task_name] ) - if tasks_cache is not None: - task_info = tasks_cache.get(task_name) - if task_info is not None: - task_type = task_info["task_type"] - output[instance_id] = task_type + return output def get_instances_context_info( @@ -1574,75 +1796,79 @@ class CreateContext: # Backwards compatibility for cases where folder name is set instead # of folder path - folder_names = set() folder_paths = set() - for folder_path in task_names_by_folder_path.keys(): + task_names_by_folder_name = {} + task_names_by_folder_path_clean = {} + for folder_path, task_names in task_names_by_folder_path.items(): if folder_path is None: - pass - elif "/" in folder_path: - folder_paths.add(folder_path) - else: - folder_names.add(folder_path) + continue - folder_paths_by_id = {} - if folder_paths: + clean_task_names = { + task_name + for task_name in task_names + if task_name + } + + if "/" not in folder_path: + task_names_by_folder_name[folder_path] = clean_task_names + continue + + folder_paths.add(folder_path) + if not clean_task_names: + continue + + task_names_by_folder_path_clean[folder_path] = clean_task_names + + folder_paths_by_name = collections.defaultdict(list) + if task_names_by_folder_name: for folder_entity in ayon_api.get_folders( project_name, - folder_paths=folder_paths, - fields={"id", "path"} + folder_names=task_names_by_folder_name.keys(), + fields={"name", "path"} ): - folder_id = folder_entity["id"] - folder_path = folder_entity["path"] - folder_paths_by_id[folder_id] = folder_path - self._folder_id_by_folder_path[folder_path] = folder_id - - folder_entities_by_name = collections.defaultdict(list) - if folder_names: - for folder_entity in ayon_api.get_folders( - project_name, - folder_names=folder_names, - fields={"id", "name", "path"} - ): - folder_id = folder_entity["id"] folder_name = folder_entity["name"] folder_path = folder_entity["path"] - folder_paths_by_id[folder_id] = folder_path - folder_entities_by_name[folder_name].append(folder_entity) - self._folder_id_by_folder_path[folder_path] = folder_id + folder_paths_by_name[folder_name].append(folder_path) - tasks_entities = ayon_api.get_tasks( - project_name, - folder_ids=folder_paths_by_id.keys(), - fields={"name", "folderId"} + folder_path_by_name = {} + for folder_name, paths in folder_paths_by_name.items(): + if len(paths) != 1: + continue + path = paths[0] + folder_path_by_name[folder_name] = path + folder_paths.add(path) + clean_task_names = task_names_by_folder_name[folder_name] + if not clean_task_names: + continue + folder_task_names = task_names_by_folder_path_clean.setdefault( + path, set() + ) + folder_task_names |= clean_task_names + + folder_entities_by_path = self.get_folder_entities(folder_paths) + task_entities_by_folder_path = self.get_task_entities( + task_names_by_folder_path_clean ) - task_names_by_folder_path = collections.defaultdict(set) - for task_entity in tasks_entities: - folder_id = task_entity["folderId"] - folder_path = folder_paths_by_id[folder_id] - task_names_by_folder_path[folder_path].add(task_entity["name"]) - - self._task_names_by_folder_path.update(task_names_by_folder_path) - for instance in to_validate: folder_path = instance["folderPath"] task_name = instance.get("task") if folder_path and "/" not in folder_path: - folder_entities = folder_entities_by_name.get(folder_path) - if len(folder_entities) == 1: - folder_path = folder_entities[0]["path"] - instance["folderPath"] = folder_path + new_folder_path = folder_path_by_name.get(folder_path) + if new_folder_path: + folder_path = new_folder_path + instance["folderPath"] = new_folder_path - if folder_path not in task_infos_by_folder_path: + folder_entity = folder_entities_by_path.get(folder_path) + if not folder_entity: continue context_info = info_by_instance_id[instance.id] context_info.folder_is_valid = True if ( not task_name - or task_name in task_infos_by_folder_path[folder_path] + or task_name in task_entities_by_folder_path[folder_path] ): - task_info = task_infos_by_folder_path[folder_path] context_info.task_is_valid = True return info_by_instance_id From 92e43fc45b81a9305589be85871011cf3236012c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Oct 2024 23:44:24 +0200 Subject: [PATCH 007/100] Fix variable names + simplify logic --- .../extract_usd_layer_contributions.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index a67c6ec702..0ffce8b643 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -464,17 +464,13 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, return [] # Attributes logic - disabled = False publish_attributes = instance["publish_attributes"].get( cls.__name__, {}) - enabled = publish_attributes.get("contribution_enabled", True) - variant_enabled = enabled and publish_attributes.get( + visible = publish_attributes.get("contribution_enabled", True) + variant_visible = visible and publish_attributes.get( "contribution_apply_as_variant", True) - disabled = not enabled - variant_disabled = not variant_enabled - return [ UISeparatorDef("usd_container_settings1"), UILabelDef(label="USD Contribution"), @@ -500,7 +496,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "department layer." ), default="usdAsset", - hidden=disabled), + visible=visible), EnumDef("contribution_target_product_init", label="Initialize as", tooltip=( @@ -512,7 +508,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, ), items=["asset", "shot"], default="asset", - hidden=disabled), + visible=visible), # Asset layer, e.g. model.usd, look.usd, rig.usd EnumDef("contribution_layer", @@ -525,7 +521,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, ), items=list(cls.contribution_layers.keys()), default="model", - hidden=disabled), + visible=visible), BoolDef("contribution_apply_as_variant", label="Add as variant", tooltip=( @@ -537,15 +533,15 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "instead." ), default=True, - hidden=disabled), + visible=visible), TextDef("contribution_variant_set_name", label="Variant Set Name", default="{layer}", - hidden=variant_disabled), + visible=variant_visible), TextDef("contribution_variant", label="Variant Name", default="{variant}", - hidden=variant_disabled), + visible=variant_visible), BoolDef("contribution_variant_is_default", label="Set as default variant selection", tooltip=( @@ -557,7 +553,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "for the same variant set have this enabled." ), default=False, - hidden=variant_disabled), + visible=variant_visible), UISeparatorDef("usd_container_settings3"), ] From 9c9f02f0d264536bb1acdb6ee7f02cd3e5ed990c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:51:31 +0200 Subject: [PATCH 008/100] implemented iter --- client/ayon_core/pipeline/create/structures.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index bcc9a87c49..ba4a373597 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -132,6 +132,10 @@ class AttributeValues: def __contains__(self, key): return key in self._attr_defs_by_key + def __iter__(self): + for key in self._attr_defs_by_key: + yield key + def get(self, key, default=None): if key in self._attr_defs_by_key: return self[key] From 11bb657d35921c42759339937ee8b82230235e98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Oct 2024 22:10:25 +0200 Subject: [PATCH 009/100] Do not try to continue with logic if the instance isn't valid for the plug-in anyway --- .../plugins/publish/extract_usd_layer_contributions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 0ffce8b643..180cb8bbf1 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -600,6 +600,10 @@ class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions): @classmethod def get_attr_defs_for_instance(cls, create_context, instance): + # Filtering of instance, if needed, can be customized + if not cls.instance_matches_plugin_families(instance): + return [] + defs = super().get_attr_defs_for_instance(create_context, instance) # Update default for department layer to look From a28f4959e6daa156defac5ad379a4abaf0105237 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:40:36 +0200 Subject: [PATCH 010/100] replace html tags with markdown --- server/settings/publish_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index cdcd28a9ce..16b1f37187 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -459,8 +459,8 @@ class ExtractReviewFilterModel(BaseSettingsModel): single_frame_filter: str = SettingsField( "everytime", # codespell:ignore everytime description=( - "Use output always / only if input is 1 frame" - " image / only if has 2+ frames or is video" + "Use output **always** / only if input **is 1 frame**" + " image / only if has **2+ frames** or **is video**" ), enum_resolver=extract_review_filter_enum ) From 54bb5c51b9eb58252f28987eeed5db6aec30a3a9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:00:27 +0200 Subject: [PATCH 011/100] udated release trigger action --- .github/workflows/release_trigger.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release_trigger.yml b/.github/workflows/release_trigger.yml index 01a3b3a682..4293e4a8e9 100644 --- a/.github/workflows/release_trigger.yml +++ b/.github/workflows/release_trigger.yml @@ -2,10 +2,23 @@ name: 🚀 Release Trigger on: workflow_dispatch: + inputs: + draft: + type: boolean + description: "Create Release Draft" + required: false + default: false + release_overwrite: + type: string + description: "Set Version Release Tag" + required: false jobs: call-release-trigger: uses: ynput/ops-repo-automation/.github/workflows/release_trigger.yml@main + with: + draft: ${{ inputs.draft }} + release_overwrite: ${{ inputs.release_overwrite }} secrets: token: ${{ secrets.YNPUT_BOT_TOKEN }} email: ${{ secrets.CI_EMAIL }} From 8208bef6f31933a1b0aef5151dc2b957e712fe03 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Oct 2024 23:06:12 +0200 Subject: [PATCH 012/100] Allow to target not only `productType` by default with attributes, but also by `families` data on created instance --- client/ayon_core/pipeline/publish/publish_plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index d2c70894cc..6a2f4c0279 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -205,9 +205,9 @@ class AYONPyblishPluginMixin: if not cls.__instanceEnabled__: return False - for _ in pyblish.logic.plugins_by_families( - [cls], [instance.product_type] - ): + families = [instance.product_type] + families.extend(instance.data.get("families", [])) + for _ in pyblish.logic.plugins_by_families([cls], families): return True return False From 9595d8fe91e27cb046f053eb86711520826c5848 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Sun, 27 Oct 2024 09:39:08 +0100 Subject: [PATCH 013/100] fix serialize of regex --- client/ayon_core/lib/attribute_definitions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 4877a45118..5daf646873 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -523,7 +523,10 @@ class TextDef(AbstractAttrDef): def serialize(self): data = super().serialize() - data["regex"] = self.regex.pattern + regex = None + if self.regex is not None: + regex = self.regex.pattern + data["regex"] = regex data["multiline"] = self.multiline data["placeholder"] = self.placeholder return data From ea2a9e0221bbfff11ce1c75e9f000ec5d4f1d33b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:56:22 +0100 Subject: [PATCH 014/100] fix also label clone --- client/ayon_core/lib/attribute_definitions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 5daf646873..34956fd33f 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -327,8 +327,8 @@ class UISeparatorDef(UIDef): class UILabelDef(UIDef): type = "label" - def __init__(self, label, key=None): - super().__init__(label=label, key=key) + def __init__(self, label, key=None, *args, **kwargs): + super().__init__(label=label, key=key, *args, **kwargs) def _def_type_compare(self, other: "UILabelDef") -> bool: return self.label == other.label From 37f2a6bb33931d2b8c536ab0c2831c8987d58648 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:43:06 +0100 Subject: [PATCH 015/100] use kwarg to pass subtype --- .../ayon_core/plugins/publish/extract_hierarchy_to_ayon.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py index 60c92aa8b1..a169affc66 100644 --- a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py +++ b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py @@ -154,7 +154,9 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): # TODO check if existing entity have 'task' type if task_entity is None: task_entity = entity_hub.add_new_task( - task_info["type"], + task_type=task_info["type"], + # TODO change 'parent_id' to 'folder_id' when ayon api + # is updated parent_id=entity.id, name=task_name ) @@ -182,7 +184,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): folder_type = "Folder" child_entity = entity_hub.add_new_folder( - folder_type, + folder_type=folder_type, parent_id=entity.id, name=child_name ) From 90a9ffa4758e841251af398004326748c71df9f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:20:03 +0100 Subject: [PATCH 016/100] fix representation entity --- client/ayon_core/pipeline/delivery.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 2a2adf984a..de89c8eec2 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -383,6 +383,13 @@ def get_representations_delivery_template_data( continue template_data = repre_entity["context"] + # Bug in 'ayon_api', 'get_representations_hierarchy' did not fully + # convert representation entity. Fixed in 'ayon_api' 1.0.10 . + if isinstance(template_data, str): + con = ayon_api.get_server_api_connection() + repre_entity = con._representation_conversion(repre_entity) + template_data = repre_entity["context"] + template_data.update(copy.deepcopy(general_template_data)) template_data.update(get_folder_template_data( repre_hierarchy.folder, project_name From 30a3aeaa86c4a6d6496b7887269069a2ea543691 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:31:00 +0100 Subject: [PATCH 017/100] remove space between version and dot --- client/ayon_core/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index de89c8eec2..1a8a8498b9 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -384,7 +384,7 @@ def get_representations_delivery_template_data( template_data = repre_entity["context"] # Bug in 'ayon_api', 'get_representations_hierarchy' did not fully - # convert representation entity. Fixed in 'ayon_api' 1.0.10 . + # convert representation entity. Fixed in 'ayon_api' 1.0.10. if isinstance(template_data, str): con = ayon_api.get_server_api_connection() repre_entity = con._representation_conversion(repre_entity) From e3022d01786a6cd8ff90ae5816c658cd91f467c2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 30 Oct 2024 12:24:12 +0000 Subject: [PATCH 018/100] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 8a7065c93c..5a3281ed01 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.4+dev" +__version__ = "1.0.5" diff --git a/package.py b/package.py index 7c5bffe81f..ec21628d04 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.4+dev" +version = "1.0.5" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index c686d685fb..6bdffc663e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.4+dev" +version = "1.0.5" description = "" authors = ["Ynput Team "] readme = "README.md" From 1af3b2d242ee10e13712afc101523a46703c9eef Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 30 Oct 2024 12:25:01 +0000 Subject: [PATCH 019/100] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 5a3281ed01..b2480af462 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.5" +__version__ = "1.0.5+dev" diff --git a/package.py b/package.py index ec21628d04..38d930189f 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.5" +version = "1.0.5+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 6bdffc663e..31acb3f8b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.5" +version = "1.0.5+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From d2c7f1db2d7f3d016bca707cb376948030ba973c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:05:35 +0100 Subject: [PATCH 020/100] remove `"root"` key from template data --- client/ayon_core/pipeline/delivery.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 1a8a8498b9..8fdf2547ba 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -409,5 +409,9 @@ def get_representations_delivery_template_data( "version": version_entity["version"], }) _merge_data(template_data, repre_entity["context"]) + + # Remove roots from temlate data to auto-fill them with anatomy data + template_data.pop("root") + output[repre_id] = template_data return output From f8d0f2ac36e07e397862ec594c570a2409dbfb6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:30:21 +0100 Subject: [PATCH 021/100] fix typo Co-authored-by: Roy Nieterau --- client/ayon_core/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 8fdf2547ba..05b78dc05b 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -410,7 +410,7 @@ def get_representations_delivery_template_data( }) _merge_data(template_data, repre_entity["context"]) - # Remove roots from temlate data to auto-fill them with anatomy data + # Remove roots from template data to auto-fill them with anatomy data template_data.pop("root") output[repre_id] = template_data From e4d0d3dfb95ea519903fb60562b8d1b9cf4f705b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:31:22 +0100 Subject: [PATCH 022/100] safe pop of root --- client/ayon_core/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 05b78dc05b..366c261e08 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -411,7 +411,7 @@ def get_representations_delivery_template_data( _merge_data(template_data, repre_entity["context"]) # Remove roots from template data to auto-fill them with anatomy data - template_data.pop("root") + template_data.pop("root", None) output[repre_id] = template_data return output From 0c64785f2749ffb60f35287e323f0b6d67b3b007 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:02:52 +0100 Subject: [PATCH 023/100] fix mapping to folder path --- client/ayon_core/pipeline/create/context.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 7c9449cb21..14133cd18b 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1584,24 +1584,24 @@ class CreateContext: folder_entities_by_path = self.get_folder_entities( missing_folder_paths ) - folder_ids = set() + folder_path_by_id = {} for folder_path, folder_entity in folder_entities_by_path.items(): if folder_entity is not None: - folder_ids.add(folder_entity["id"]) + folder_path_by_id[folder_entity["id"]] = folder_path - if not folder_ids: + if not folder_path_by_id: return output task_entities_by_parent_id = collections.defaultdict(list) for task_entity in ayon_api.get_tasks( self.project_name, - folder_ids=folder_ids + folder_ids=folder_path_by_id.keys() ): folder_id = task_entity["folderId"] task_entities_by_parent_id[folder_id].append(task_entity) for folder_id, task_entities in task_entities_by_parent_id.items(): - folder_path = self._folder_id_by_folder_path.get(folder_id) + folder_path = folder_path_by_id[folder_id] task_ids = set() task_names = set() for task_entity in task_entities: From f48425dd47dbb953c62c87d94a8a807486657d9d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:03:00 +0100 Subject: [PATCH 024/100] better variable name --- client/ayon_core/pipeline/create/context.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 14133cd18b..4b7e323737 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1482,14 +1482,14 @@ class CreateContext: for folder_path in folder_paths if folder_path is not None } - remainders = set() + remainder_paths = set() for folder_path in output: # Skip empty/invalid folder paths if folder_path is None or "/" not in folder_path: continue if folder_path not in self._folder_id_by_folder_path: - remainders.add(folder_path) + remainder_paths.add(folder_path) continue folder_id = self._folder_id_by_folder_path.get(folder_path) @@ -1501,15 +1501,15 @@ class CreateContext: if folder_entity: output[folder_path] = folder_entity else: - remainders.add(folder_path) + remainder_paths.add(folder_path) - if not remainders: + if not remainder_paths: return output folder_paths_by_id = {} for folder_entity in ayon_api.get_folders( self.project_name, - folder_paths=remainders, + folder_paths=remainder_paths, ): folder_id = folder_entity["id"] folder_path = folder_entity["path"] From 31c37efce2501fbdf1cc78e2ce7d2b75e4ca1b46 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:18:43 +0100 Subject: [PATCH 025/100] use single variable to cache folders --- client/ayon_core/pipeline/create/context.py | 39 ++++++++------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 4b7e323737..d7fdad6fdb 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -254,9 +254,8 @@ class CreateContext: self._collection_shared_data = None # Entities cache - self._folder_entities_by_id = {} + self._folder_entities_by_path = {} self._task_entities_by_id = {} - self._folder_id_by_folder_path = {} self._task_ids_by_folder_path = {} self._task_names_by_folder_path = {} @@ -560,10 +559,9 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} - self._folder_entities_by_id = {} + self._folder_entities_by_path = {} self._task_entities_by_id = {} - self._folder_id_by_folder_path = {} self._task_ids_by_folder_path = {} self._task_names_by_folder_path = {} @@ -1485,38 +1483,31 @@ class CreateContext: remainder_paths = set() for folder_path in output: # Skip empty/invalid folder paths - if folder_path is None or "/" not in folder_path: + if "/" not in folder_path: continue - if folder_path not in self._folder_id_by_folder_path: + if folder_path not in self._folder_entities_by_path: remainder_paths.add(folder_path) continue - folder_id = self._folder_id_by_folder_path.get(folder_path) - if not folder_id: - output[folder_path] = None - continue - - folder_entity = self._folder_entities_by_id.get(folder_id) - if folder_entity: - output[folder_path] = folder_entity - else: - remainder_paths.add(folder_path) + output[folder_path] = self._folder_entities_by_path[folder_path] if not remainder_paths: return output - folder_paths_by_id = {} + found_paths = set() for folder_entity in ayon_api.get_folders( self.project_name, folder_paths=remainder_paths, ): - folder_id = folder_entity["id"] folder_path = folder_entity["path"] - folder_paths_by_id[folder_id] = folder_path + found_paths.add(folder_path) output[folder_path] = folder_entity - self._folder_entities_by_id[folder_id] = folder_entity - self._folder_id_by_folder_path[folder_path] = folder_id + self._folder_entities_by_path[folder_path] = folder_entity + + # Cache empty folders + for path in remainder_paths - found_paths: + self._folder_entities_by_path[path] = None return output @@ -1775,9 +1766,9 @@ class CreateContext: if not folder_path: continue - if folder_path in self._folder_id_by_folder_path: - folder_id = self._folder_id_by_folder_path[folder_path] - if folder_id is None: + if folder_path in self._folder_entities_by_path: + folder_entity = self._folder_entities_by_path[folder_path] + if folder_entity is None: continue context_info.folder_is_valid = True From d860919b22cb7832df2963bf9c23d2aabfcad919 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:21:31 +0100 Subject: [PATCH 026/100] remove unnecessary check --- client/ayon_core/pipeline/create/context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index d7fdad6fdb..a0145dee29 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1478,11 +1478,11 @@ class CreateContext: output = { folder_path: None for folder_path in folder_paths - if folder_path is not None } remainder_paths = set() for folder_path in output: - # Skip empty/invalid folder paths + # Skip invalid folder paths (e.g. if only folder name + # is passed in) if "/" not in folder_path: continue @@ -1505,7 +1505,7 @@ class CreateContext: output[folder_path] = folder_entity self._folder_entities_by_path[folder_path] = folder_entity - # Cache empty folders + # Cache empty folder entities for path in remainder_paths - found_paths: self._folder_entities_by_path[path] = None From ce4e8a1b04acb42bfd81ca65b4f17d6270e940ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:22:47 +0100 Subject: [PATCH 027/100] handle empty paths --- client/ayon_core/pipeline/create/context.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a0145dee29..3cff5e03b1 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1481,9 +1481,8 @@ class CreateContext: } remainder_paths = set() for folder_path in output: - # Skip invalid folder paths (e.g. if only folder name - # is passed in) - if "/" not in folder_path: + # Skip invalid folder paths (folder name or empty path) + if not folder_path or "/" not in folder_path: continue if folder_path not in self._folder_entities_by_path: From b4009b718ae6bdcfe398361dc3cd4f1176cfcee1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:23:15 +0100 Subject: [PATCH 028/100] discard None from folder paths --- client/ayon_core/pipeline/create/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3cff5e03b1..6bfd64b822 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1666,6 +1666,7 @@ class CreateContext: instance.get("folderPath") for instance in instances } + folder_paths.discard(None) folder_entities_by_path = self.get_folder_entities(folder_paths) for instance in instances: folder_path = instance.get("folderPath") From eb561dd371cb0ae7825ac2fc281ffb7fd1cfe720 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:07:31 +0100 Subject: [PATCH 029/100] Remove `os.link` compatibility for Windows. Support for Windows exists since Py 3.2 See: https://docs.python.org/3/library/os.html#os.link --- client/ayon_core/lib/path_tools.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index 5c81fbfebf..efe2556afe 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -38,31 +38,7 @@ def create_hard_link(src_path, dst_path): dst_path(str): Full path to a file where a link of source will be added. """ - # Use `os.link` if is available - # - should be for all platforms with newer python versions - if hasattr(os, "link"): - os.link(src_path, dst_path) - return - - # Windows implementation of hardlinks - # - used in Python 2 - if platform.system().lower() == "windows": - import ctypes - from ctypes.wintypes import BOOL - CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW - CreateHardLink.argtypes = [ - ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p - ] - CreateHardLink.restype = BOOL - - res = CreateHardLink(dst_path, src_path, None) - if res == 0: - raise ctypes.WinError() - return - # Raises not implemented error if gets here - raise NotImplementedError( - "Implementation of hardlink for current environment is missing." - ) + os.link(src_path, dst_path) def collect_frames(files): From 0a970abed6bcccb2755f70af189959ca00c6b3d9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:11:52 +0100 Subject: [PATCH 030/100] Remove Python 2 code. `unicode` does not exist in Py3+ --- client/ayon_core/lib/log.py | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 36c39f9d84..1ed16b36ff 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -11,12 +11,12 @@ import copy from . import Terminal -# Check for `unicode` in builtins -USE_UNICODE = hasattr(__builtins__, "unicode") - class LogStreamHandler(logging.StreamHandler): - """ StreamHandler class designed to handle utf errors in python 2.x hosts. + """StreamHandler class. + + This was originally designed to handle UTF errors in python 2.x hosts, + however currently solely remains for backwards compatibility. """ @@ -47,27 +47,7 @@ class LogStreamHandler(logging.StreamHandler): stream = self.stream if stream is None: return - fs = "%s\n" - # if no unicode support... - if not USE_UNICODE: - stream.write(fs % msg) - else: - try: - if (isinstance(msg, unicode) and # noqa: F821 - getattr(stream, 'encoding', None)): - ufs = u'%s\n' - try: - stream.write(ufs % msg) - except UnicodeEncodeError: - stream.write((ufs % msg).encode(stream.encoding)) - else: - if (getattr(stream, 'encoding', 'utf-8')): - ufs = u'%s\n' - stream.write(ufs % unicode(msg)) # noqa: F821 - else: - stream.write(fs % msg) - except UnicodeError: - stream.write(fs % msg.encode("UTF-8")) + stream.write(f"{msg}\n") self.flush() except (KeyboardInterrupt, SystemExit): raise From 9f9c03179a6ab3bec5dfe3f8e757be6baf8cc055 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:16:16 +0100 Subject: [PATCH 031/100] Fix enabled check + fix docstrings --- client/ayon_core/lib/log.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 1ed16b36ff..d619310eb8 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -25,21 +25,21 @@ class LogStreamHandler(logging.StreamHandler): self.enabled = True def enable(self): - """ Enable StreamHandler + """Enable StreamHandler - Used to silence output + Make StreamHandler output again """ self.enabled = True def disable(self): - """ Disable StreamHandler + """Disable StreamHandler - Make StreamHandler output again + Used to silence output """ self.enabled = False def emit(self, record): - if not self.enable: + if not self.enabled: return try: msg = self.format(record) From 9343e3cca98bf9a3f546efdfd77b43a5e78d9da3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:20:27 +0100 Subject: [PATCH 032/100] Remove `Logger.mongo_process_id` --- client/ayon_core/lib/log.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 36c39f9d84..03edc93c74 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -141,8 +141,6 @@ class Logger: process_data = None # Cached process name or ability to set different process name _process_name = None - # TODO Remove 'mongo_process_id' in 1.x.x - mongo_process_id = uuid.uuid4().hex @classmethod def get_logger(cls, name=None): From 300e086c8d4075b39e5627e1a68e7cfa6a6c8793 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:28:59 +0100 Subject: [PATCH 033/100] Update client/ayon_core/lib/log.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index d619310eb8..d392610f32 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -39,7 +39,7 @@ class LogStreamHandler(logging.StreamHandler): self.enabled = False def emit(self, record): - if not self.enabled: + if not self.enabled or self.stream is None: return try: msg = self.format(record) From 01bd1594f5bbabb21cfb85ee312064bbbe9c7a7b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:29:31 +0100 Subject: [PATCH 034/100] Remove condition that was moved up --- client/ayon_core/lib/log.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index d392610f32..692df93dad 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -45,8 +45,6 @@ class LogStreamHandler(logging.StreamHandler): msg = self.format(record) msg = Terminal.log(msg) stream = self.stream - if stream is None: - return stream.write(f"{msg}\n") self.flush() except (KeyboardInterrupt, SystemExit): From d4ace0706c90e032bdea5139cdc8e690b2f2c673 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:30:53 +0100 Subject: [PATCH 035/100] Remove unused import --- client/ayon_core/lib/path_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index efe2556afe..c6a833f43a 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -1,7 +1,6 @@ import os import re import logging -import platform import clique From df9821f928af8f26863f502737f863cd39778ef9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:31:21 +0100 Subject: [PATCH 036/100] Fix typo --- client/ayon_core/lib/path_tools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index c6a833f43a..ebd22b43c5 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -185,7 +185,7 @@ def get_last_version_from_path(path_dir, filter): assert isinstance(filter, list) and ( len(filter) != 0), "`filter` argument needs to be list and not empty" - filtred_files = list() + filtered_files = list() # form regex for filtering pattern = r".*".join(filter) @@ -193,10 +193,10 @@ def get_last_version_from_path(path_dir, filter): for file in os.listdir(path_dir): if not re.findall(pattern, file): continue - filtred_files.append(file) + filtered_files.append(file) - if filtred_files: - sorted(filtred_files) - return filtred_files[-1] + if filtered_files: + sorted(filtered_files) + return filtered_files[-1] return None From a02954d908507538e62de6818d3339ee945ce4fe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 23:19:23 +0100 Subject: [PATCH 037/100] Fix sorting Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/path_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index ebd22b43c5..31baac168c 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -196,7 +196,7 @@ def get_last_version_from_path(path_dir, filter): filtered_files.append(file) if filtered_files: - sorted(filtered_files) + filtered_files.sort() return filtered_files[-1] return None From 8c87a66cfe29c1eb8dc4a059364a38e3175aebf1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 1 Nov 2024 12:45:13 +0100 Subject: [PATCH 038/100] Remove unused import --- client/ayon_core/lib/log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index f48d3767c9..0c2fe5e2d4 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -1,6 +1,5 @@ import os import sys -import uuid import getpass import logging import platform From 1d053c6b7cee154b7e0fc19aa841428c632e60e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:40:04 +0100 Subject: [PATCH 039/100] action upload to ynput cloud --- .github/workflows/upload_to_ynput_cloud.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/upload_to_ynput_cloud.yml diff --git a/.github/workflows/upload_to_ynput_cloud.yml b/.github/workflows/upload_to_ynput_cloud.yml new file mode 100644 index 0000000000..7745a8e016 --- /dev/null +++ b/.github/workflows/upload_to_ynput_cloud.yml @@ -0,0 +1,16 @@ +name: 📤 Upload to Ynput Cloud + +on: + workflow_dispatch: + release: + types: [published] + +jobs: + call-upload-to-ynput-cloud: + uses: ynput/ops-repo-automation/.github/workflows/upload_to_ynput_cloud.yml@main + secrets: + CI_EMAIL: ${{ secrets.CI_EMAIL }} + CI_USER: ${{ secrets.CI_USER }} + YNPUT_BOT_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }} + YNPUT_CLOUD_URL: ${{ secrets.YNPUT_CLOUD_URL }} + YNPUT_CLOUD_TOKEN: ${{ secrets.YNPUT_CLOUD_TOKEN }} From ead5d9963a63413f917ad21329979f7e1147de79 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Nov 2024 14:42:52 +0000 Subject: [PATCH 040/100] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index b2480af462..af93f3e660 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.5+dev" +__version__ = "1.0.6" diff --git a/package.py b/package.py index 38d930189f..b3bc01d0d0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.5+dev" +version = "1.0.6" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 31acb3f8b4..cccc81c069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.5+dev" +version = "1.0.6" description = "" authors = ["Ynput Team "] readme = "README.md" From 348614745a8fc383d16bdf408f5eaa35bc124999 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Nov 2024 14:43:31 +0000 Subject: [PATCH 041/100] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index af93f3e660..2b2af81e18 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.6" +__version__ = "1.0.6+dev" diff --git a/package.py b/package.py index b3bc01d0d0..59f0e82be0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.6" +version = "1.0.6+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index cccc81c069..ca626eff00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.6" +version = "1.0.6+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 7d2e676f522d956c406adc021ced1287004d1b1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:17:57 +0100 Subject: [PATCH 042/100] create attributes widget can show overriden values --- .../publisher/widgets/product_attributes.py | 67 ++++++++++++++++--- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 61d5ca111d..b49f846ffa 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -1,6 +1,9 @@ +from typing import Dict + from qtpy import QtWidgets, QtCore from ayon_core.lib.attribute_definitions import UnknownDef +from ayon_core.tools.utils import set_style_property from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend from ayon_core.tools.publisher.constants import ( @@ -9,6 +12,22 @@ from ayon_core.tools.publisher.constants import ( ) +def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): + set_style_property( + label, + "overriden", + "1" if overriden else "" + ) + + +class _CreateAttrDefInfo: + def __init__(self, attr_def, instance_ids, defaults, label_widget): + self.attr_def = attr_def + self.instance_ids = instance_ids + self.defaults = defaults + self.label_widget = label_widget + + class CreatorAttrsWidget(QtWidgets.QWidget): """Widget showing creator specific attributes for selected instances. @@ -51,8 +70,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._controller: AbstractPublisherFrontend = controller self._scroll_area = scroll_area - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} + self._attr_def_info_by_id: Dict[str, _CreateAttrDefInfo] = {} self._current_instance_ids = set() # To store content of scroll area to prevent garbage collection @@ -81,8 +99,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): prev_content_widget.deleteLater() self._content_widget = None - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} + self._attr_def_info_by_id = {} result = self._controller.get_creator_attribute_definitions( self._current_instance_ids @@ -97,9 +114,20 @@ class CreatorAttrsWidget(QtWidgets.QWidget): content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) row = 0 - for attr_def, instance_ids, values in result: + for attr_def, info_by_id in result: widget = create_widget_for_attr_def(attr_def, content_widget) + default_values = set() if attr_def.is_value_def: + default_values = [] + values = [] + for item in info_by_id.values(): + values.append(item["value"]) + # 'set' cannot be used for default values because they can + # be unhashable types, e.g. 'list'. + default = item["default"] + if default not in default_values: + default_values.append(default) + if len(values) == 1: value = values[0] if value is not None: @@ -108,8 +136,10 @@ class CreatorAttrsWidget(QtWidgets.QWidget): widget.set_value(values, True) widget.value_changed.connect(self._input_value_changed) - self._attr_def_id_to_instances[attr_def.id] = instance_ids - self._attr_def_id_to_attr_def[attr_def.id] = attr_def + attr_def_info = _CreateAttrDefInfo( + attr_def, list(info_by_id), default_values, None + ) + self._attr_def_info_by_id[attr_def.id] = attr_def_info if not attr_def.visible: continue @@ -121,8 +151,14 @@ class CreatorAttrsWidget(QtWidgets.QWidget): col_num = 2 - expand_cols label = None + is_overriden = False if attr_def.is_value_def: + is_overriden = any( + item["value"] != item["default"] + for item in info_by_id.values() + ) label = attr_def.label or attr_def.key + if label: label_widget = QtWidgets.QLabel(label, self) tooltip = attr_def.tooltip @@ -138,6 +174,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): ) if not attr_def.is_label_horizontal: row += 1 + attr_def_info.label_widget = label_widget + _set_label_overriden(label_widget, is_overriden) content_layout.addWidget( widget, row, col_num, 1, expand_cols @@ -165,12 +203,19 @@ class CreatorAttrsWidget(QtWidgets.QWidget): break def _input_value_changed(self, value, attr_id): - instance_ids = self._attr_def_id_to_instances.get(attr_id) - attr_def = self._attr_def_id_to_attr_def.get(attr_id) - if not instance_ids or not attr_def: + attr_def_info = self._attr_def_info_by_id.get(attr_id) + if attr_def_info is None: return + + if attr_def_info.label_widget is not None: + defaults = attr_def_info.defaults + is_overriden = len(defaults) != 1 or value not in defaults + _set_label_overriden(attr_def_info.label_widget, is_overriden) + self._controller.set_instances_create_attr_values( - instance_ids, attr_def.key, value + attr_def_info.instance_ids, + attr_def_info.attr_def.key, + value ) From ecc7d2bde9d2755a74248c05a81482d2fe906c29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:18:15 +0100 Subject: [PATCH 043/100] change return type of 'get_creator_attribute_definitions' --- client/ayon_core/tools/publisher/abstract.py | 2 +- .../tools/publisher/models/create.py | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index a6ae93cecd..5de3f72de1 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -366,7 +366,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): @abstractmethod def get_creator_attribute_definitions( self, instance_ids: Iterable[str] - ) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]: + ) -> List[Tuple[AbstractAttrDef, Dict[str, Dict[str, Any]]]]: pass @abstractmethod diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 9c13d8ae2f..e4c208f1e8 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -769,7 +769,7 @@ class CreateModel: def get_creator_attribute_definitions( self, instance_ids: List[str] - ) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]: + ) -> List[Tuple[AbstractAttrDef, Dict[str, Dict[str, Any]]]]: """Collect creator attribute definitions for multuple instances. Args: @@ -796,12 +796,23 @@ class CreateModel: if found_idx is None: idx = len(output) - output.append((attr_def, [instance_id], [value])) + output.append(( + attr_def, + { + instance_id: { + "value": value, + "default": attr_def.default + } + } + )) _attr_defs[idx] = attr_def else: - _, ids, values = output[found_idx] - ids.append(instance_id) - values.append(value) + _, info_by_id = output[found_idx] + info_by_id[instance_id] = { + "value": value, + "default": attr_def.default + } + return output def set_instances_publish_attr_values( From a1f74f2c783473fd2692efb7a9321dad36f87199 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:04:52 +0100 Subject: [PATCH 044/100] added styling to overriden label --- client/ayon_core/style/data.json | 4 ++++ client/ayon_core/style/style.css | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index 7389387d97..d4a8b6180b 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -60,7 +60,11 @@ "icon-alert-tools": "#AA5050", "icon-entity-default": "#bfccd6", "icon-entity-disabled": "#808080", + "font-entity-deprecated": "#666666", + + "font-overridden": "#FF8C1A", + "overlay-messages": { "close-btn": "#D3D8DE", "bg-success": "#458056", diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 3d84d917a4..0d1d4f710e 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -44,6 +44,10 @@ QLabel { background: transparent; } +QLabel[overriden="1"] { + color: {color:font-overridden}; +} + /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; From 0e0620770ff03039346e1eb29852531dbceed42c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:32:14 +0100 Subject: [PATCH 045/100] fix variable definition --- client/ayon_core/tools/publisher/widgets/product_attributes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index b49f846ffa..ab41812e4e 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -116,9 +116,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): row = 0 for attr_def, info_by_id in result: widget = create_widget_for_attr_def(attr_def, content_widget) - default_values = set() + default_values = [] if attr_def.is_value_def: - default_values = [] values = [] for item in info_by_id.values(): values.append(item["value"]) From 9484c42b9a646690769c21b1794f354b49ee7fee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:09:46 +0100 Subject: [PATCH 046/100] 'get_publish_attribute_definitions' returns default values too --- client/ayon_core/tools/publisher/abstract.py | 2 +- .../ayon_core/tools/publisher/models/create.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 5de3f72de1..7fad2b8176 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -383,7 +383,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ) -> List[Tuple[ str, List[AbstractAttrDef], - Dict[str, List[Tuple[str, Any]]] + Dict[str, List[Tuple[str, Any, Any]]] ]]: pass diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index e4c208f1e8..8763d79712 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -846,7 +846,7 @@ class CreateModel: ) -> List[Tuple[ str, List[AbstractAttrDef], - Dict[str, List[Tuple[str, Any]]] + Dict[str, List[Tuple[str, Any, Any]]] ]]: """Collect publish attribute definitions for passed instances. @@ -876,21 +876,21 @@ class CreateModel: attr_defs = attr_val.attr_defs if not attr_defs: continue + plugin_attr_defs = all_defs_by_plugin_name.setdefault( plugin_name, [] ) - plugin_attr_defs.append(attr_defs) - plugin_values = all_plugin_values.setdefault(plugin_name, {}) + plugin_attr_defs.append(attr_defs) + for attr_def in attr_defs: if isinstance(attr_def, UIDef): continue - attr_values = plugin_values.setdefault(attr_def.key, []) - - value = attr_val[attr_def.key] - attr_values.append((item_id, value)) + attr_values.append( + (item_id, attr_val[attr_def.key], attr_def.default) + ) attr_defs_by_plugin_name = {} for plugin_name, attr_defs in all_defs_by_plugin_name.items(): @@ -904,7 +904,7 @@ class CreateModel: output.append(( plugin_name, attr_defs_by_plugin_name[plugin_name], - all_plugin_values + all_plugin_values[plugin_name], )) return output From d99dff1610f4243f8217a35968d73c54e7a24804 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:10:01 +0100 Subject: [PATCH 047/100] modified publish attributes to display overrides --- .../publisher/widgets/product_attributes.py | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index ab41812e4e..206eebc6f1 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -1,8 +1,9 @@ -from typing import Dict +import typing +from typing import Dict, List, Any from qtpy import QtWidgets, QtCore -from ayon_core.lib.attribute_definitions import UnknownDef +from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef from ayon_core.tools.utils import set_style_property from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend @@ -11,6 +12,9 @@ from ayon_core.tools.publisher.constants import ( INPUTS_LAYOUT_VSPACING, ) +if typing.TYPE_CHECKING: + from typing import Union + def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): set_style_property( @@ -28,6 +32,22 @@ class _CreateAttrDefInfo: self.label_widget = label_widget +class _PublishAttrDefInfo: + def __init__( + self, + attr_def: AbstractAttrDef, + plugin_name: str, + instance_ids: List["Union[str, None]"], + defaults: List[Any], + label_widget: "Union[None, QtWidgets.QLabel]", + ): + self.attr_def = attr_def + self.plugin_name = plugin_name + self.instance_ids = instance_ids + self.defaults = defaults + self.label_widget = label_widget + + class CreatorAttrsWidget(QtWidgets.QWidget): """Widget showing creator specific attributes for selected instances. @@ -267,9 +287,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._controller: AbstractPublisherFrontend = controller self._scroll_area = scroll_area - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} - self._attr_def_id_to_plugin_name = {} + self._attr_def_info_by_id: Dict[str, _PublishAttrDefInfo] = {} # Store content of scroll area to prevent garbage collection self._content_widget = None @@ -298,9 +316,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._content_widget = None - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} - self._attr_def_id_to_plugin_name = {} + self._attr_def_info_by_id = {} result = self._controller.get_publish_attribute_definitions( self._current_instance_ids, self._context_selected @@ -319,9 +335,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): content_layout.addStretch(1) row = 0 - for plugin_name, attr_defs, all_plugin_values in result: - plugin_values = all_plugin_values[plugin_name] - + for plugin_name, attr_defs, plugin_values in result: for attr_def in attr_defs: widget = create_widget_for_attr_def( attr_def, content_widget @@ -334,6 +348,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget.setVisible(False) visible_widget = False + label_widget = None if visible_widget: expand_cols = 2 if attr_def.is_value_def and attr_def.is_label_horizontal: @@ -368,35 +383,58 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget.value_changed.connect(self._input_value_changed) - attr_values = plugin_values[attr_def.key] - multivalue = len(attr_values) > 1 + instance_ids = [] values = [] - instances = [] - for instance, value in attr_values: + default_values = [] + is_overriden = False + for (instance_id, value, default_value) in ( + plugin_values.get(attr_def.key, []) + ): + instance_ids.append(instance_id) values.append(value) - instances.append(instance) + if not is_overriden and value != default_value: + is_overriden = True + # 'set' cannot be used for default values because they can + # be unhashable types, e.g. 'list'. + if default_value not in default_values: + default_values.append(default_value) - self._attr_def_id_to_attr_def[attr_def.id] = attr_def - self._attr_def_id_to_instances[attr_def.id] = instances - self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name + multivalue = len(values) > 1 + + self._attr_def_info_by_id[attr_def.id] = _PublishAttrDefInfo( + attr_def, + plugin_name, + instance_ids, + default_values, + label_widget, + ) if multivalue: widget.set_value(values, multivalue) else: widget.set_value(values[0]) + if label_widget is not None: + _set_label_overriden(label_widget, is_overriden) + self._scroll_area.setWidget(content_widget) self._content_widget = content_widget def _input_value_changed(self, value, attr_id): - instance_ids = self._attr_def_id_to_instances.get(attr_id) - attr_def = self._attr_def_id_to_attr_def.get(attr_id) - plugin_name = self._attr_def_id_to_plugin_name.get(attr_id) - if not instance_ids or not attr_def or not plugin_name: + attr_def_info = self._attr_def_info_by_id.get(attr_id) + if attr_def_info is None: return + if attr_def_info.label_widget is not None: + defaults = attr_def_info.defaults + is_overriden = len(defaults) != 1 or value not in defaults + _set_label_overriden(attr_def_info.label_widget, is_overriden) + self._controller.set_instances_publish_attr_values( - instance_ids, plugin_name, attr_def.key, value + attr_def_info.instance_ids, + attr_def_info.plugin_name, + attr_def_info.attr_def.key, + value ) def _on_instance_attr_defs_change(self, event): From 98674eb4366bb063d9d09789223b3403db74f7bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:07:40 +0100 Subject: [PATCH 048/100] change overriden color --- client/ayon_core/style/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index d4a8b6180b..748a51238a 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -63,7 +63,7 @@ "font-entity-deprecated": "#666666", - "font-overridden": "#FF8C1A", + "font-overridden": "#33B461", "overlay-messages": { "close-btn": "#D3D8DE", From 46acbacaac83de7245404e978fe9d9423aa5b36a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:51:29 +0100 Subject: [PATCH 049/100] added typehints and tiny docstring --- .../publisher/widgets/product_attributes.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 206eebc6f1..b0b2640640 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -25,14 +25,22 @@ def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): class _CreateAttrDefInfo: - def __init__(self, attr_def, instance_ids, defaults, label_widget): - self.attr_def = attr_def - self.instance_ids = instance_ids - self.defaults = defaults - self.label_widget = label_widget + """Helper class to store information about create attribute definition.""" + def __init__( + self, + attr_def: AbstractAttrDef, + instance_ids: List["Union[str, None]"], + defaults: List[Any], + label_widget: "Union[None, QtWidgets.QLabel]", + ): + self.attr_def: AbstractAttrDef = attr_def + self.instance_ids: List["Union[str, None]"] = instance_ids + self.defaults: List[Any] = defaults + self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget class _PublishAttrDefInfo: + """Helper class to store information about publish attribute definition.""" def __init__( self, attr_def: AbstractAttrDef, @@ -41,11 +49,11 @@ class _PublishAttrDefInfo: defaults: List[Any], label_widget: "Union[None, QtWidgets.QLabel]", ): - self.attr_def = attr_def - self.plugin_name = plugin_name - self.instance_ids = instance_ids - self.defaults = defaults - self.label_widget = label_widget + self.attr_def: AbstractAttrDef = attr_def + self.plugin_name: str = plugin_name + self.instance_ids: List["Union[str, None]"] = instance_ids + self.defaults: List[Any] = defaults + self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget class CreatorAttrsWidget(QtWidgets.QWidget): From 994dc956c264daccffb5b8bbe7aa8786589be2d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:16:57 +0100 Subject: [PATCH 050/100] use blue color --- client/ayon_core/style/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index 748a51238a..24629ec085 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -63,7 +63,7 @@ "font-entity-deprecated": "#666666", - "font-overridden": "#33B461", + "font-overridden": "#91CDFC", "overlay-messages": { "close-btn": "#D3D8DE", From 3a8a9ec4831be2b72d6ae3f7a2ba35fc20a32482 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:25:20 +0100 Subject: [PATCH 051/100] implemented logic to revert to default values --- client/ayon_core/tools/publisher/abstract.py | 17 ++++ client/ayon_core/tools/publisher/control.py | 12 +++ .../tools/publisher/models/create.py | 98 ++++++++++++------- 3 files changed, 93 insertions(+), 34 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 7fad2b8176..4ed91813d3 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -375,6 +375,14 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ): pass + @abstractmethod + def revert_instances_create_attr_values( + self, + instance_ids: List["Union[str, None]"], + key: str, + ): + pass + @abstractmethod def get_publish_attribute_definitions( self, @@ -397,6 +405,15 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ): pass + @abstractmethod + def revert_instances_publish_attr_values( + self, + instance_ids: List["Union[str, None]"], + plugin_name: str, + key: str, + ): + pass + @abstractmethod def get_product_name( self, diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 347755d557..98fdda08cf 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -412,6 +412,11 @@ class PublisherController( instance_ids, key, value ) + def revert_instances_create_attr_values(self, instance_ids, key): + self._create_model.revert_instances_create_attr_values( + instance_ids, key + ) + def get_publish_attribute_definitions(self, instance_ids, include_context): """Collect publish attribute definitions for passed instances. @@ -432,6 +437,13 @@ class PublisherController( instance_ids, plugin_name, key, value ) + def revert_instances_publish_attr_values( + self, instance_ids, plugin_name, key + ): + return self._create_model.revert_instances_publish_attr_values( + instance_ids, plugin_name, key + ) + def get_product_name( self, creator_identifier, diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 8763d79712..ca26749b65 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -40,6 +40,7 @@ from ayon_core.tools.publisher.abstract import ( ) CREATE_EVENT_SOURCE = "publisher.create.model" +_DEFAULT_VALUE = object() class CreatorType: @@ -752,20 +753,12 @@ class CreateModel: self._remove_instances_from_context(instance_ids) def set_instances_create_attr_values(self, instance_ids, key, value): - with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): - for instance_id in instance_ids: - instance = self._get_instance_by_id(instance_id) - creator_attributes = instance["creator_attributes"] - attr_def = creator_attributes.get_attr_def(key) - if ( - attr_def is None - or not attr_def.is_value_def - or not attr_def.visible - or not attr_def.enabled - or not attr_def.is_value_valid(value) - ): - continue - creator_attributes[key] = value + self._set_instances_create_attr_values(instance_ids, key, value) + + def revert_instances_create_attr_values(self, instance_ids, key): + self._set_instances_create_attr_values( + instance_ids, key, _DEFAULT_VALUE + ) def get_creator_attribute_definitions( self, instance_ids: List[str] @@ -816,28 +809,18 @@ class CreateModel: return output def set_instances_publish_attr_values( - self, instance_ids, plugin_name, key, value + self, instance_ids, plugin_name, key, value ): - with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): - for instance_id in instance_ids: - if instance_id is None: - instance = self._create_context - else: - instance = self._get_instance_by_id(instance_id) - plugin_val = instance.publish_attributes[plugin_name] - attr_def = plugin_val.get_attr_def(key) - # Ignore if attribute is not available or enabled/visible - # on the instance, or the value is not valid for definition - if ( - attr_def is None - or not attr_def.is_value_def - or not attr_def.visible - or not attr_def.enabled - or not attr_def.is_value_valid(value) - ): - continue + self._set_instances_publish_attr_values( + instance_ids, plugin_name, key, value + ) - plugin_val[key] = value + def revert_instances_publish_attr_values( + self, instance_ids, plugin_name, key + ): + self._set_instances_publish_attr_values( + instance_ids, plugin_name, key, _DEFAULT_VALUE + ) def get_publish_attribute_definitions( self, @@ -1064,6 +1047,53 @@ class CreateModel: CreatorItem.from_creator(creator) ) + def _set_instances_create_attr_values(self, instance_ids, key, value): + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id in instance_ids: + instance = self._get_instance_by_id(instance_id) + creator_attributes = instance["creator_attributes"] + attr_def = creator_attributes.get_attr_def(key) + if ( + attr_def is None + or not attr_def.is_value_def + or not attr_def.visible + or not attr_def.enabled + ): + continue + + if value is _DEFAULT_VALUE: + creator_attributes[key] = attr_def.default + + elif attr_def.is_value_valid(value): + creator_attributes[key] = value + + def _set_instances_publish_attr_values( + self, instance_ids, plugin_name, key, value + ): + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id in instance_ids: + if instance_id is None: + instance = self._create_context + else: + instance = self._get_instance_by_id(instance_id) + plugin_val = instance.publish_attributes[plugin_name] + attr_def = plugin_val.get_attr_def(key) + # Ignore if attribute is not available or enabled/visible + # on the instance, or the value is not valid for definition + if ( + attr_def is None + or not attr_def.is_value_def + or not attr_def.visible + or not attr_def.enabled + ): + continue + + if value is _DEFAULT_VALUE: + plugin_val[key] = attr_def.default + + elif attr_def.is_value_valid(value): + plugin_val[key] = value + def _cc_added_instance(self, event): instance_ids = { instance.id From 56a07fe9183eaa33ad202261afb2d880499d2805 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:28:30 +0100 Subject: [PATCH 052/100] added 'AttributeDefinitionsLabel' helper label widget --- .../ayon_core/tools/attribute_defs/widgets.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 026aea00ad..e1977cca2c 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -20,11 +20,14 @@ from ayon_core.tools.utils import ( FocusSpinBox, FocusDoubleSpinBox, MultiSelectionComboBox, + set_style_property, ) from ayon_core.tools.utils import NiceCheckbox from .files_widget import FilesWidget +_REVERT_TO_DEFAULT_LABEL = "Revert to default" + def create_widget_for_attr_def(attr_def, parent=None): widget = _create_widget_for_attr_def(attr_def, parent) @@ -74,6 +77,52 @@ def _create_widget_for_attr_def(attr_def, parent=None): )) +class AttributeDefinitionsLabel(QtWidgets.QLabel): + """Label related to value attribute definition. + + Label is used to show attribute definition label and to show if value + is overridden. + + Label can be right-clicked to revert value to default. + """ + revert_to_default_requested = QtCore.Signal(str) + + def __init__( + self, + attr_id: str, + label: str, + parent: QtWidgets.QWidget, + ): + super().__init__(label, parent) + + self._attr_id = attr_id + self._overridden = False + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + self.customContextMenuRequested.connect(self._on_context_menu) + + def set_overridden(self, overridden: bool): + if self._overridden == overridden: + return + self._overridden = overridden + set_style_property( + self, + "overridden", + "1" if overridden else "" + ) + + def _on_context_menu(self, point: QtCore.QPoint): + menu = QtWidgets.QMenu(self) + action = QtWidgets.QAction(menu) + action.setText(_REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self._request_revert_to_default) + menu.addAction(action) + menu.exec_(self.mapToGlobal(point)) + + def _request_revert_to_default(self): + self.revert_to_default_requested.emit(self._attr_id) + + class AttributeDefinitionsWidget(QtWidgets.QWidget): """Create widgets for attribute definitions in grid layout. From 33a5195b7156e444139232b55545058f34e173bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:28:46 +0100 Subject: [PATCH 053/100] added 'AttributeDefinitionsLabel' to init --- client/ayon_core/tools/attribute_defs/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/attribute_defs/__init__.py b/client/ayon_core/tools/attribute_defs/__init__.py index f991fdec3d..7f6cbb41be 100644 --- a/client/ayon_core/tools/attribute_defs/__init__.py +++ b/client/ayon_core/tools/attribute_defs/__init__.py @@ -1,6 +1,7 @@ from .widgets import ( create_widget_for_attr_def, AttributeDefinitionsWidget, + AttributeDefinitionsLabel, ) from .dialog import ( @@ -11,6 +12,7 @@ from .dialog import ( __all__ = ( "create_widget_for_attr_def", "AttributeDefinitionsWidget", + "AttributeDefinitionsLabel", "AttributeDefinitionsDialog", ) From 7d0f1f97e4ded1cc2a855e7db51fcceb2affc560 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:29:48 +0100 Subject: [PATCH 054/100] use new label in product attributes --- .../publisher/widgets/product_attributes.py | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index b0b2640640..07cbfb1907 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -4,8 +4,10 @@ from typing import Dict, List, Any from qtpy import QtWidgets, QtCore from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef -from ayon_core.tools.utils import set_style_property -from ayon_core.tools.attribute_defs import create_widget_for_attr_def +from ayon_core.tools.attribute_defs import ( + create_widget_for_attr_def, + AttributeDefinitionsLabel, +) from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend from ayon_core.tools.publisher.constants import ( INPUTS_LAYOUT_HSPACING, @@ -16,14 +18,6 @@ if typing.TYPE_CHECKING: from typing import Union -def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): - set_style_property( - label, - "overriden", - "1" if overriden else "" - ) - - class _CreateAttrDefInfo: """Helper class to store information about create attribute definition.""" def __init__( @@ -31,12 +25,14 @@ class _CreateAttrDefInfo: attr_def: AbstractAttrDef, instance_ids: List["Union[str, None]"], defaults: List[Any], - label_widget: "Union[None, QtWidgets.QLabel]", + label_widget: "Union[AttributeDefinitionsLabel, None]", ): self.attr_def: AbstractAttrDef = attr_def self.instance_ids: List["Union[str, None]"] = instance_ids self.defaults: List[Any] = defaults - self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget + self.label_widget: "Union[AttributeDefinitionsLabel, None]" = ( + label_widget + ) class _PublishAttrDefInfo: @@ -47,13 +43,15 @@ class _PublishAttrDefInfo: plugin_name: str, instance_ids: List["Union[str, None]"], defaults: List[Any], - label_widget: "Union[None, QtWidgets.QLabel]", + label_widget: "Union[AttributeDefinitionsLabel, None]", ): self.attr_def: AbstractAttrDef = attr_def self.plugin_name: str = plugin_name self.instance_ids: List["Union[str, None]"] = instance_ids self.defaults: List[Any] = defaults - self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget + self.label_widget: "Union[AttributeDefinitionsLabel, None]" = ( + label_widget + ) class CreatorAttrsWidget(QtWidgets.QWidget): @@ -187,7 +185,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): label = attr_def.label or attr_def.key if label: - label_widget = QtWidgets.QLabel(label, self) + label_widget = AttributeDefinitionsLabel( + attr_def.id, label, self + ) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -202,7 +202,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): if not attr_def.is_label_horizontal: row += 1 attr_def_info.label_widget = label_widget - _set_label_overriden(label_widget, is_overriden) + label_widget.set_overridden(is_overriden) content_layout.addWidget( widget, row, col_num, 1, expand_cols @@ -237,7 +237,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): if attr_def_info.label_widget is not None: defaults = attr_def_info.defaults is_overriden = len(defaults) != 1 or value not in defaults - _set_label_overriden(attr_def_info.label_widget, is_overriden) + attr_def_info.label_widget.set_overridden(is_overriden) self._controller.set_instances_create_attr_values( attr_def_info.instance_ids, @@ -367,7 +367,9 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): if attr_def.is_value_def: label = attr_def.label or attr_def.key if label: - label_widget = QtWidgets.QLabel(label, content_widget) + label_widget = AttributeDefinitionsLabel( + attr_def.id, label, content_widget + ) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -423,7 +425,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget.set_value(values[0]) if label_widget is not None: - _set_label_overriden(label_widget, is_overriden) + label_widget.set_overridden(is_overriden) self._scroll_area.setWidget(content_widget) self._content_widget = content_widget @@ -436,7 +438,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): if attr_def_info.label_widget is not None: defaults = attr_def_info.defaults is_overriden = len(defaults) != 1 or value not in defaults - _set_label_overriden(attr_def_info.label_widget, is_overriden) + attr_def_info.label_widget.set_overridden(is_overriden) self._controller.set_instances_publish_attr_values( attr_def_info.instance_ids, From 5569c95aefb02dec9ef1760fa6dd7a0eb497707b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:29:56 +0100 Subject: [PATCH 055/100] change style of label --- client/ayon_core/style/style.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 0d1d4f710e..bd96a3aeed 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -44,10 +44,6 @@ QLabel { background: transparent; } -QLabel[overriden="1"] { - color: {color:font-overridden}; -} - /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; @@ -1589,6 +1585,10 @@ CreateNextPageOverlay { } /* Attribute Definition widgets */ +AttributeDefinitionsLabel[overridden="1"] { + color: {color:font-overridden}; +} + AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { padding: 1px; } From 9de6a74789a4d8ba92b66806b1005e37e3a977b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:31:23 +0100 Subject: [PATCH 056/100] base attribute widget can handle reset to default logic --- .../ayon_core/tools/attribute_defs/widgets.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index e1977cca2c..09637a9696 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -241,11 +241,18 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): class _BaseAttrDefWidget(QtWidgets.QWidget): # Type 'object' may not work with older PySide versions value_changed = QtCore.Signal(object, str) + revert_to_default_requested = QtCore.Signal(str) - def __init__(self, attr_def, parent): - super(_BaseAttrDefWidget, self).__init__(parent) + def __init__( + self, + attr_def: AbstractAttrDef, + parent: "Union[QtWidgets.QWidget, None]", + handle_revert_to_default: Optional[bool] = True, + ): + super().__init__(parent) - self.attr_def = attr_def + self.attr_def: AbstractAttrDef = attr_def + self._handle_revert_to_default: bool = handle_revert_to_default main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -254,6 +261,15 @@ class _BaseAttrDefWidget(QtWidgets.QWidget): self._ui_init() + def revert_to_default_value(self): + if not self.attr_def.is_value_def: + return + + if self._handle_revert_to_default: + self.set_value(self.attr_def.default) + else: + self.revert_to_default_requested.emit(self.attr_def.id) + def _ui_init(self): raise NotImplementedError( "Method '_ui_init' is not implemented. {}".format( From bbe1d9e3fd1b8e6ca284614ccf5e6ee5d212efd5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:32:19 +0100 Subject: [PATCH 057/100] 'create_widget_for_attr_def' can pass in all init args --- .../ayon_core/tools/attribute_defs/widgets.py | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 09637a9696..d3f51a196c 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -1,4 +1,6 @@ import copy +import typing +from typing import Optional from qtpy import QtWidgets, QtCore @@ -26,11 +28,20 @@ from ayon_core.tools.utils import NiceCheckbox from .files_widget import FilesWidget +if typing.TYPE_CHECKING: + from typing import Union + _REVERT_TO_DEFAULT_LABEL = "Revert to default" -def create_widget_for_attr_def(attr_def, parent=None): - widget = _create_widget_for_attr_def(attr_def, parent) +def create_widget_for_attr_def( + attr_def: AbstractAttrDef, + parent: Optional[QtWidgets.QWidget] = None, + handle_revert_to_default: Optional[bool] = True, +): + widget = _create_widget_for_attr_def( + attr_def, parent, handle_revert_to_default + ) if not attr_def.visible: widget.setVisible(False) @@ -39,42 +50,50 @@ def create_widget_for_attr_def(attr_def, parent=None): return widget -def _create_widget_for_attr_def(attr_def, parent=None): +def _create_widget_for_attr_def( + attr_def: AbstractAttrDef, + parent: "Union[QtWidgets.QWidget, None]", + handle_revert_to_default: bool, +): if not isinstance(attr_def, AbstractAttrDef): raise TypeError("Unexpected type \"{}\" expected \"{}\"".format( str(type(attr_def)), AbstractAttrDef )) + cls = None if isinstance(attr_def, NumberDef): - return NumberAttrWidget(attr_def, parent) + cls = NumberAttrWidget - if isinstance(attr_def, TextDef): - return TextAttrWidget(attr_def, parent) + elif isinstance(attr_def, TextDef): + cls = TextAttrWidget - if isinstance(attr_def, EnumDef): - return EnumAttrWidget(attr_def, parent) + elif isinstance(attr_def, EnumDef): + cls = EnumAttrWidget - if isinstance(attr_def, BoolDef): - return BoolAttrWidget(attr_def, parent) + elif isinstance(attr_def, BoolDef): + cls = BoolAttrWidget - if isinstance(attr_def, UnknownDef): - return UnknownAttrWidget(attr_def, parent) + elif isinstance(attr_def, UnknownDef): + cls = UnknownAttrWidget - if isinstance(attr_def, HiddenDef): - return HiddenAttrWidget(attr_def, parent) + elif isinstance(attr_def, HiddenDef): + cls = HiddenAttrWidget - if isinstance(attr_def, FileDef): - return FileAttrWidget(attr_def, parent) + elif isinstance(attr_def, FileDef): + cls = FileAttrWidget - if isinstance(attr_def, UISeparatorDef): - return SeparatorAttrWidget(attr_def, parent) + elif isinstance(attr_def, UISeparatorDef): + cls = SeparatorAttrWidget - if isinstance(attr_def, UILabelDef): - return LabelAttrWidget(attr_def, parent) + elif isinstance(attr_def, UILabelDef): + cls = LabelAttrWidget - raise ValueError("Unknown attribute definition \"{}\"".format( - str(type(attr_def)) - )) + if cls is None: + raise ValueError("Unknown attribute definition \"{}\"".format( + str(type(attr_def)) + )) + + return cls(attr_def, parent, handle_revert_to_default) class AttributeDefinitionsLabel(QtWidgets.QLabel): From c23cf6746d8014f46921fcc610d894b36f027d43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:32:58 +0100 Subject: [PATCH 058/100] 'AttributeDefinitionsWidget' shows overriden values on labels --- .../ayon_core/tools/attribute_defs/widgets.py | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index d3f51a196c..94121e51bc 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -151,16 +151,18 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): """ def __init__(self, attr_defs=None, parent=None): - super(AttributeDefinitionsWidget, self).__init__(parent) + super().__init__(parent) - self._widgets = [] + self._widgets_by_id = {} + self._labels_by_id = {} self._current_keys = set() self.set_attr_defs(attr_defs) def clear_attr_defs(self): """Remove all existing widgets and reset layout if needed.""" - self._widgets = [] + self._widgets_by_id = {} + self._labels_by_id = {} self._current_keys = set() layout = self.layout() @@ -202,6 +204,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): self._current_keys.add(attr_def.key) widget = create_widget_for_attr_def(attr_def, self) self._widgets.append(widget) + self._widgets_by_id[attr_def.id] = widget if not attr_def.visible: continue @@ -213,7 +216,13 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): col_num = 2 - expand_cols if attr_def.is_value_def and attr_def.label: - label_widget = QtWidgets.QLabel(attr_def.label, self) + label_widget = AttributeDefinitionsLabel( + attr_def.id, attr_def.label, self + ) + label_widget.revert_to_default_requested.connect( + self._on_revert_request + ) + self._labels_by_id[attr_def.id] = label_widget tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -228,6 +237,9 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): if not attr_def.is_label_horizontal: row += 1 + if attr_def.is_value_def: + widget.value_changed.connect(self._on_value_change) + layout.addWidget( widget, row, col_num, 1, expand_cols ) @@ -236,7 +248,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): def set_value(self, value): new_value = copy.deepcopy(value) unused_keys = set(new_value.keys()) - for widget in self._widgets: + for widget in self._widgets_by_id.values(): attr_def = widget.attr_def if attr_def.key not in new_value: continue @@ -249,13 +261,26 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): def current_value(self): output = {} - for widget in self._widgets: + for widget in self._widgets_by_id.values(): attr_def = widget.attr_def if not isinstance(attr_def, UIDef): output[attr_def.key] = widget.current_value() return output + def _on_revert_request(self, attr_id): + widget = self._widgets_by_id.get(attr_id) + if widget is not None: + widget.set_value(widget.attr_def.default) + + def _on_value_change(self, value, attr_id): + widget = self._widgets_by_id.get(attr_id) + if widget is None: + return + label = self._labels_by_id.get(attr_id) + if label is not None: + label.set_overridden(value != widget.attr_def.default) + class _BaseAttrDefWidget(QtWidgets.QWidget): # Type 'object' may not work with older PySide versions From 47973464fdd7fd923a208d8b44b78da15bcd69f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:33:38 +0100 Subject: [PATCH 059/100] remoe python 2 super calls --- client/ayon_core/tools/attribute_defs/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 94121e51bc..118f4b5f64 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -364,7 +364,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit): clicked = QtCore.Signal() def __init__(self, text, parent): - super(ClickableLineEdit, self).__init__(parent) + super().__init__(parent) self.setText(text) self.setReadOnly(True) @@ -373,7 +373,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit): def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self._mouse_pressed = True - super(ClickableLineEdit, self).mousePressEvent(event) + super().mousePressEvent(event) def mouseReleaseEvent(self, event): if self._mouse_pressed: @@ -381,7 +381,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit): if self.rect().contains(event.pos()): self.clicked.emit() - super(ClickableLineEdit, self).mouseReleaseEvent(event) + super().mouseReleaseEvent(event) class NumberAttrWidget(_BaseAttrDefWidget): @@ -596,7 +596,7 @@ class BoolAttrWidget(_BaseAttrDefWidget): class EnumAttrWidget(_BaseAttrDefWidget): def __init__(self, *args, **kwargs): self._multivalue = False - super(EnumAttrWidget, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) @property def multiselection(self): @@ -723,7 +723,7 @@ class HiddenAttrWidget(_BaseAttrDefWidget): def setVisible(self, visible): if visible: visible = False - super(HiddenAttrWidget, self).setVisible(visible) + super().setVisible(visible) def current_value(self): if self._multivalue: From b5d018c071341474e91c97f60b389a21e45f30b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:34:41 +0100 Subject: [PATCH 060/100] publisher does handle revert to default --- .../publisher/widgets/product_attributes.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 07cbfb1907..cb165d1be7 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -141,7 +141,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): row = 0 for attr_def, info_by_id in result: - widget = create_widget_for_attr_def(attr_def, content_widget) + widget = create_widget_for_attr_def( + attr_def, content_widget, handle_revert_to_default=False + ) default_values = [] if attr_def.is_value_def: values = [] @@ -161,6 +163,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): widget.set_value(values, True) widget.value_changed.connect(self._input_value_changed) + widget.revert_to_default_requested.connect( + self._on_request_revert_to_default + ) attr_def_info = _CreateAttrDefInfo( attr_def, list(info_by_id), default_values, None ) @@ -203,6 +208,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): row += 1 attr_def_info.label_widget = label_widget label_widget.set_overridden(is_overriden) + label_widget.revert_to_default_requested.connect( + self._on_request_revert_to_default + ) content_layout.addWidget( widget, row, col_num, 1, expand_cols @@ -245,6 +253,15 @@ class CreatorAttrsWidget(QtWidgets.QWidget): value ) + def _on_request_revert_to_default(self, attr_id): + attr_def_info = self._attr_def_info_by_id.get(attr_id) + if attr_def_info is None: + return + self._controller.revert_instances_create_attr_values( + attr_def_info.instance_ids, + attr_def_info.attr_def.key, + ) + class PublishPluginAttrsWidget(QtWidgets.QWidget): """Widget showing publish plugin attributes for selected instances. @@ -346,7 +363,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): for plugin_name, attr_defs, plugin_values in result: for attr_def in attr_defs: widget = create_widget_for_attr_def( - attr_def, content_widget + attr_def, content_widget, handle_revert_to_default=False ) visible_widget = attr_def.visible # Hide unknown values of publish plugins @@ -370,6 +387,9 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): label_widget = AttributeDefinitionsLabel( attr_def.id, label, content_widget ) + label_widget.revert_to_default_requested.connect( + self._on_request_revert_to_default + ) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -392,6 +412,9 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): continue widget.value_changed.connect(self._input_value_changed) + widget.revert_to_default_requested.connect( + self._on_request_revert_to_default + ) instance_ids = [] values = [] @@ -447,6 +470,17 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): value ) + def _on_request_revert_to_default(self, attr_id): + attr_def_info = self._attr_def_info_by_id.get(attr_id) + if attr_def_info is None: + return + + self._controller.revert_instances_publish_attr_values( + attr_def_info.instance_ids, + attr_def_info.plugin_name, + attr_def_info.attr_def.key, + ) + def _on_instance_attr_defs_change(self, event): for instance_id in event.data: if ( From 2d51436da71fb9b6b95409779bc1f33715e837af Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:46:35 +0100 Subject: [PATCH 061/100] refresh content --- client/ayon_core/tools/publisher/widgets/product_attributes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index cb165d1be7..3ff295c986 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -261,6 +261,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): attr_def_info.instance_ids, attr_def_info.attr_def.key, ) + self._refresh_content() class PublishPluginAttrsWidget(QtWidgets.QWidget): @@ -480,6 +481,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): attr_def_info.plugin_name, attr_def_info.attr_def.key, ) + self._refresh_content() def _on_instance_attr_defs_change(self, event): for instance_id in event.data: From 9be42980bdb46578b6a04a7424d1a04b165e507e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:58:21 +0100 Subject: [PATCH 062/100] implemented request restart logic for most of widgets --- .../tools/attribute_defs/_constants.py | 1 + .../ayon_core/tools/attribute_defs/widgets.py | 58 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 client/ayon_core/tools/attribute_defs/_constants.py diff --git a/client/ayon_core/tools/attribute_defs/_constants.py b/client/ayon_core/tools/attribute_defs/_constants.py new file mode 100644 index 0000000000..b58a05bac6 --- /dev/null +++ b/client/ayon_core/tools/attribute_defs/_constants.py @@ -0,0 +1 @@ +REVERT_TO_DEFAULT_LABEL = "Revert to default" diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 118f4b5f64..03482c1006 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -26,13 +26,12 @@ from ayon_core.tools.utils import ( ) from ayon_core.tools.utils import NiceCheckbox +from ._constants import REVERT_TO_DEFAULT_LABEL from .files_widget import FilesWidget if typing.TYPE_CHECKING: from typing import Union -_REVERT_TO_DEFAULT_LABEL = "Revert to default" - def create_widget_for_attr_def( attr_def: AbstractAttrDef, @@ -133,7 +132,7 @@ class AttributeDefinitionsLabel(QtWidgets.QLabel): def _on_context_menu(self, point: QtCore.QPoint): menu = QtWidgets.QMenu(self) action = QtWidgets.QAction(menu) - action.setText(_REVERT_TO_DEFAULT_LABEL) + action.setText(REVERT_TO_DEFAULT_LABEL) action.triggered.connect(self._request_revert_to_default) menu.addAction(action) menu.exec_(self.mapToGlobal(point)) @@ -393,6 +392,9 @@ class NumberAttrWidget(_BaseAttrDefWidget): else: input_widget = FocusSpinBox(self) + # Override context menu event to add revert to default action + input_widget.contextMenuEvent = self._input_widget_context_event + if self.attr_def.tooltip: input_widget.setToolTip(self.attr_def.tooltip) @@ -430,6 +432,16 @@ class NumberAttrWidget(_BaseAttrDefWidget): self._set_multiselection_visible(True) return False + def _input_widget_context_event(self, event): + line_edit = self._input_widget.lineEdit() + menu = line_edit.createStandardContextMenu() + menu.setAttribute(QtCore.Qt.WA_DeleteOnClose) + action = QtWidgets.QAction(menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + menu.addAction(action) + menu.popup(event.globalPos()) + def current_value(self): return self._input_widget.value() @@ -495,6 +507,9 @@ class TextAttrWidget(_BaseAttrDefWidget): else: input_widget = QtWidgets.QLineEdit(self) + # Override context menu event to add revert to default action + input_widget.contextMenuEvent = self._input_widget_context_event + if ( self.attr_def.placeholder and hasattr(input_widget, "setPlaceholderText") @@ -516,6 +531,15 @@ class TextAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) + def _input_widget_context_event(self, event): + menu = self._input_widget.createStandardContextMenu() + menu.setAttribute(QtCore.Qt.WA_DeleteOnClose) + action = QtWidgets.QAction(menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + menu.addAction(action) + menu.popup(event.globalPos()) + def _on_value_change(self): if self.multiline: new_value = self._input_widget.toPlainText() @@ -568,6 +592,20 @@ class BoolAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) self.main_layout.addStretch(1) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._on_context_menu) + + def _on_context_menu(self, pos): + self._menu = QtWidgets.QMenu(self) + + action = QtWidgets.QAction(self._menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + self._menu.addAction(action) + + global_pos = self.mapToGlobal(pos) + self._menu.exec_(global_pos) + def _on_value_change(self): new_value = self._input_widget.isChecked() self.value_changed.emit(new_value, self.attr_def.id) @@ -631,6 +669,20 @@ class EnumAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) + input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + input_widget.customContextMenuRequested.connect(self._on_context_menu) + + def _on_context_menu(self, pos): + menu = QtWidgets.QMenu(self) + + action = QtWidgets.QAction(menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + menu.addAction(action) + + global_pos = self.mapToGlobal(pos) + menu.exec_(global_pos) + def _on_value_change(self): new_value = self.current_value() if self._multivalue: From cc45af7a96023b4ee9d39e81968bf0cce2290508 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:58:55 +0100 Subject: [PATCH 063/100] implemented request revert on files widget --- .../tools/attribute_defs/files_widget.py | 72 ++++++++++++------- .../ayon_core/tools/attribute_defs/widgets.py | 15 ++++ 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index 95091bed5a..46399c5fce 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -17,6 +17,8 @@ from ayon_core.tools.utils import ( PixmapLabel ) +from ._constants import REVERT_TO_DEFAULT_LABEL + ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 ITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2 ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3 @@ -598,7 +600,7 @@ class FilesView(QtWidgets.QListView): """View showing instances and their groups.""" remove_requested = QtCore.Signal() - context_menu_requested = QtCore.Signal(QtCore.QPoint) + context_menu_requested = QtCore.Signal(QtCore.QPoint, bool) def __init__(self, *args, **kwargs): super(FilesView, self).__init__(*args, **kwargs) @@ -690,9 +692,8 @@ class FilesView(QtWidgets.QListView): def _on_context_menu_request(self, pos): index = self.indexAt(pos) - if index.isValid(): - point = self.viewport().mapToGlobal(pos) - self.context_menu_requested.emit(point) + point = self.viewport().mapToGlobal(pos) + self.context_menu_requested.emit(point, index.isValid()) def _on_selection_change(self): self._remove_btn.setEnabled(self.has_selected_item_ids()) @@ -721,27 +722,34 @@ class FilesView(QtWidgets.QListView): class FilesWidget(QtWidgets.QFrame): value_changed = QtCore.Signal() + revert_requested = QtCore.Signal() def __init__(self, single_item, allow_sequences, extensions_label, parent): - super(FilesWidget, self).__init__(parent) + super().__init__(parent) self.setAcceptDrops(True) + wrapper_widget = QtWidgets.QWidget(self) + empty_widget = DropEmpty( - single_item, allow_sequences, extensions_label, self + single_item, allow_sequences, extensions_label, wrapper_widget ) files_model = FilesModel(single_item, allow_sequences) files_proxy_model = FilesProxyModel() files_proxy_model.setSourceModel(files_model) - files_view = FilesView(self) + files_view = FilesView(wrapper_widget) files_view.setModel(files_proxy_model) - layout = QtWidgets.QStackedLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setStackingMode(QtWidgets.QStackedLayout.StackAll) - layout.addWidget(empty_widget) - layout.addWidget(files_view) - layout.setCurrentWidget(empty_widget) + wrapper_layout = QtWidgets.QStackedLayout(wrapper_widget) + wrapper_layout.setContentsMargins(0, 0, 0, 0) + wrapper_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll) + wrapper_layout.addWidget(empty_widget) + wrapper_layout.addWidget(files_view) + wrapper_layout.setCurrentWidget(empty_widget) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(wrapper_widget, 1) files_proxy_model.rowsInserted.connect(self._on_rows_inserted) files_proxy_model.rowsRemoved.connect(self._on_rows_removed) @@ -761,7 +769,11 @@ class FilesWidget(QtWidgets.QFrame): self._widgets_by_id = {} - self._layout = layout + self._wrapper_widget = wrapper_widget + self._wrapper_layout = wrapper_layout + + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._on_context_menu) def _set_multivalue(self, multivalue): if self._multivalue is multivalue: @@ -770,7 +782,7 @@ class FilesWidget(QtWidgets.QFrame): self._files_view.set_multivalue(multivalue) self._files_model.set_multivalue(multivalue) self._files_proxy_model.set_multivalue(multivalue) - self.setEnabled(not multivalue) + self._wrapper_widget.setEnabled(not multivalue) def set_value(self, value, multivalue): self._in_set_value = True @@ -888,22 +900,28 @@ class FilesWidget(QtWidgets.QFrame): if items_to_delete: self._remove_item_by_ids(items_to_delete) - def _on_context_menu_requested(self, pos): - if self._multivalue: - return + def _on_context_menu(self, pos): + self._on_context_menu_requested(pos, False) + def _on_context_menu_requested(self, pos, valid_index): menu = QtWidgets.QMenu(self._files_view) + if valid_index and not self._multivalue: + if self._files_view.has_selected_sequence(): + split_action = QtWidgets.QAction("Split sequence", menu) + split_action.triggered.connect(self._on_split_request) + menu.addAction(split_action) - if self._files_view.has_selected_sequence(): - split_action = QtWidgets.QAction("Split sequence", menu) - split_action.triggered.connect(self._on_split_request) - menu.addAction(split_action) + remove_action = QtWidgets.QAction("Remove", menu) + remove_action.triggered.connect(self._on_remove_requested) + menu.addAction(remove_action) - remove_action = QtWidgets.QAction("Remove", menu) - remove_action.triggered.connect(self._on_remove_requested) - menu.addAction(remove_action) + if not valid_index: + revert_action = QtWidgets.QAction(REVERT_TO_DEFAULT_LABEL, menu) + revert_action.triggered.connect(self.revert_requested) + menu.addAction(revert_action) - menu.popup(pos) + if menu.actions(): + menu.popup(pos) def dragEnterEvent(self, event): if self._multivalue: @@ -1011,5 +1029,5 @@ class FilesWidget(QtWidgets.QFrame): current_widget = self._files_view else: current_widget = self._empty_widget - self._layout.setCurrentWidget(current_widget) + self._wrapper_layout.setCurrentWidget(current_widget) self._files_view.update_remove_btn_visibility() diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 03482c1006..22f4bfe535 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -811,10 +811,25 @@ class FileAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) + input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + input_widget.customContextMenuRequested.connect(self._on_context_menu) + input_widget.revert_requested.connect(self.revert_to_default_value) + def _on_value_change(self): new_value = self.current_value() self.value_changed.emit(new_value, self.attr_def.id) + def _on_context_menu(self, pos): + menu = QtWidgets.QMenu(self) + + action = QtWidgets.QAction(menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + menu.addAction(action) + + global_pos = self.mapToGlobal(pos) + menu.exec_(global_pos) + def current_value(self): return self._input_widget.current_value() From 53a839b34fcf04669e094e728448e95ba792d4f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:15:09 +0100 Subject: [PATCH 064/100] fix condition triggering refresh of values in UI --- .../ayon_core/tools/publisher/widgets/product_attributes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 3ff295c986..2b9f316d41 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -232,7 +232,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): for instance_id, changes in event["instance_changes"].items(): if ( instance_id in self._current_instance_ids - and "creator_attributes" not in changes + and "creator_attributes" in changes ): self._refresh_content() break @@ -498,7 +498,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): for instance_id, changes in event["instance_changes"].items(): if ( instance_id in self._current_instance_ids - and "publish_attributes" not in changes + and "publish_attributes" in changes ): self._refresh_content() break From 569ce30b9672c77e6d553a03098160c0c13e166c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:38:44 +0100 Subject: [PATCH 065/100] pass all required arguments to FileDefItem --- client/ayon_core/lib/attribute_definitions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 34956fd33f..789c878d40 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -966,7 +966,9 @@ class FileDef(AbstractAttrDef): FileDefItem.from_dict(default) elif isinstance(default, str): - default = FileDefItem.from_paths([default.strip()])[0] + default = FileDefItem.from_paths( + [default.strip()], allow_sequences + )[0] else: raise TypeError(( @@ -1044,7 +1046,9 @@ class FileDef(AbstractAttrDef): pass if string_paths: - file_items = FileDefItem.from_paths(string_paths) + file_items = FileDefItem.from_paths( + string_paths, self.allow_sequences + ) dict_items.extend([ file_item.to_dict() for file_item in file_items From 521d8ed9ec87df5487480ebbfdfac5b31f7dfab4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:28:07 +0100 Subject: [PATCH 066/100] move register functions below classes --- client/ayon_core/lib/attribute_definitions.py | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 34956fd33f..e4e998189d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -15,67 +15,6 @@ import clique _attr_defs_by_type = {} -def register_attr_def_class(cls): - """Register attribute definition. - - Currently registered definitions are used to deserialize data to objects. - - Attrs: - cls (AbstractAttrDef): Non-abstract class to be registered with unique - 'type' attribute. - - Raises: - KeyError: When type was already registered. - """ - - if cls.type in _attr_defs_by_type: - raise KeyError("Type \"{}\" was already registered".format(cls.type)) - _attr_defs_by_type[cls.type] = cls - - -def get_attributes_keys(attribute_definitions): - """Collect keys from list of attribute definitions. - - Args: - attribute_definitions (List[AbstractAttrDef]): Objects of attribute - definitions. - - Returns: - Set[str]: Keys that will be created using passed attribute definitions. - """ - - keys = set() - if not attribute_definitions: - return keys - - for attribute_def in attribute_definitions: - if not isinstance(attribute_def, UIDef): - keys.add(attribute_def.key) - return keys - - -def get_default_values(attribute_definitions): - """Receive default values for attribute definitions. - - Args: - attribute_definitions (List[AbstractAttrDef]): Attribute definitions - for which default values should be collected. - - Returns: - Dict[str, Any]: Default values for passed attribute definitions. - """ - - output = {} - if not attribute_definitions: - return output - - for attr_def in attribute_definitions: - # Skip UI definitions - if not isinstance(attr_def, UIDef): - output[attr_def.key] = attr_def.default - return output - - class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. @@ -1062,6 +1001,67 @@ class FileDef(AbstractAttrDef): return [] +def register_attr_def_class(cls): + """Register attribute definition. + + Currently registered definitions are used to deserialize data to objects. + + Attrs: + cls (AbstractAttrDef): Non-abstract class to be registered with unique + 'type' attribute. + + Raises: + KeyError: When type was already registered. + """ + + if cls.type in _attr_defs_by_type: + raise KeyError("Type \"{}\" was already registered".format(cls.type)) + _attr_defs_by_type[cls.type] = cls + + +def get_attributes_keys(attribute_definitions): + """Collect keys from list of attribute definitions. + + Args: + attribute_definitions (List[AbstractAttrDef]): Objects of attribute + definitions. + + Returns: + Set[str]: Keys that will be created using passed attribute definitions. + """ + + keys = set() + if not attribute_definitions: + return keys + + for attribute_def in attribute_definitions: + if not isinstance(attribute_def, UIDef): + keys.add(attribute_def.key) + return keys + + +def get_default_values(attribute_definitions): + """Receive default values for attribute definitions. + + Args: + attribute_definitions (List[AbstractAttrDef]): Attribute definitions + for which default values should be collected. + + Returns: + Dict[str, Any]: Default values for passed attribute definitions. + """ + + output = {} + if not attribute_definitions: + return output + + for attr_def in attribute_definitions: + # Skip UI definitions + if not isinstance(attr_def, UIDef): + output[attr_def.key] = attr_def.default + return output + + def serialize_attr_def(attr_def): """Serialize attribute definition to data. From 9d629eca2fd87903afd28d2998c4522f8be67fd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:30:55 +0100 Subject: [PATCH 067/100] added helper type definitions --- client/ayon_core/lib/attribute_definitions.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e4e998189d..76abe5fe4d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -6,14 +6,33 @@ import json import copy import warnings from abc import ABCMeta, abstractmethod -from typing import Any, Optional +import typing +from typing import Any, Optional, List, TypedDict import clique +if typing.TYPE_CHECKING: + from typing import Union # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} +# Type hint helpers +IntFloatType = "Union[int, float]" + + +class EnumItemDict(TypedDict): + label: str + value: Any + + +class FileDefItemDict(TypedDict): + directory: str + filenames: List[str] + frames: Optional[List[int]] + template: Optional[str] + is_sequence: Optional[bool] + class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. From 443ebf8523adbbee9112c5f4aa26980f6c3122ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:43:46 +0100 Subject: [PATCH 068/100] added most of typehints --- client/ayon_core/lib/attribute_definitions.py | 177 ++++++++++++------ 1 file changed, 117 insertions(+), 60 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 76abe5fe4d..82c7ab9cb1 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -7,12 +7,14 @@ import copy import warnings from abc import ABCMeta, abstractmethod import typing -from typing import Any, Optional, List, TypedDict +from typing import ( + Any, Optional, List, Set, Dict, Iterable, TypedDict, TypeVar, +) import clique if typing.TYPE_CHECKING: - from typing import Union + from typing import Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} @@ -51,8 +53,12 @@ class AbstractAttrDefMeta(ABCMeta): def _convert_reversed_attr( - main_value, depr_value, main_label, depr_label, default -): + main_value: Any, + depr_value: Any, + main_label: str, + depr_label: str, + default: Any, +) -> Any: if main_value is not None and depr_value is not None: if main_value == depr_value: print( @@ -141,7 +147,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def id(self) -> str: return self._id - def clone(self): + def clone(self) -> "Self": data = self.serialize() data.pop("type") return self.deserialize(data) @@ -214,7 +220,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): pass @abstractmethod - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: """Convert value to a valid one. Convert passed value to a valid type. Use default if value can't be @@ -223,7 +229,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): pass - def serialize(self): + def serialize(self) -> Dict[str, Any]: """Serialize object to data so it's possible to recreate it. Returns: @@ -246,7 +252,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return data @classmethod - def deserialize(cls, data): + def deserialize(cls, data: Dict[str, Any]) -> "Self": """Recreate object from data. Data can be received using 'serialize' method. @@ -257,7 +263,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return cls(**data) - def _def_type_compare(self, other: "AbstractAttrDef") -> bool: + def _def_type_compare(self, other: "Self") -> bool: return True @@ -268,13 +274,19 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): class UIDef(AbstractAttrDef): is_value_def = False - def __init__(self, key=None, default=None, *args, **kwargs): + def __init__( + self, + key: Optional[str] = None, + default: Optional[Any] = None, + *args, + **kwargs + ): super().__init__(key, default, *args, **kwargs) def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -305,14 +317,14 @@ class UnknownDef(AbstractAttrDef): type = "unknown" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[Any] = None, **kwargs): kwargs["default"] = default super().__init__(key, **kwargs) def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -327,7 +339,7 @@ class HiddenDef(AbstractAttrDef): type = "hidden" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[Any] = None, **kwargs): kwargs["default"] = default kwargs["visible"] = False super().__init__(key, **kwargs) @@ -335,7 +347,7 @@ class HiddenDef(AbstractAttrDef): def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -360,7 +372,12 @@ class NumberDef(AbstractAttrDef): ] def __init__( - self, key, minimum=None, maximum=None, decimals=None, default=None, + self, + key: str, + minimum: Optional[IntFloatType] = None, + maximum: Optional[IntFloatType] = None, + decimals: Optional[int] = None, + default: Optional[IntFloatType] = None, **kwargs ): minimum = 0 if minimum is None else minimum @@ -386,9 +403,9 @@ class NumberDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.minimum = minimum - self.maximum = maximum - self.decimals = 0 if decimals is None else decimals + self.minimum: IntFloatType = minimum + self.maximum: IntFloatType = maximum + self.decimals: int = 0 if decimals is None else decimals def is_value_valid(self, value: Any) -> bool: if self.decimals == 0: @@ -400,7 +417,7 @@ class NumberDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value(self, value: Any) -> IntFloatType: if isinstance(value, str): try: value = float(value) @@ -444,7 +461,12 @@ class TextDef(AbstractAttrDef): ] def __init__( - self, key, multiline=None, regex=None, placeholder=None, default=None, + self, + key: str, + multiline: Optional[bool] = None, + regex: Optional[str] = None, + placeholder: Optional[str] = None, + default: Optional[str] = None, **kwargs ): if default is None: @@ -463,9 +485,9 @@ class TextDef(AbstractAttrDef): if isinstance(regex, str): regex = re.compile(regex) - self.multiline = multiline - self.placeholder = placeholder - self.regex = regex + self.multiline: bool = multiline + self.placeholder: Optional[str] = placeholder + self.regex: Optional["Pattern"] = regex def is_value_valid(self, value: Any) -> bool: if not isinstance(value, str): @@ -474,12 +496,12 @@ class TextDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value(self, value: Any) -> str: if isinstance(value, str): return value return self.default - def serialize(self): + def serialize(self) -> Dict[str, Any]: data = super().serialize() regex = None if self.regex is not None: @@ -503,8 +525,9 @@ class EnumDef(AbstractAttrDef): is enabled. Args: - items (Union[list[str], list[dict[str, Any]]): Items definition that - can be converted using 'prepare_enum_items'. + key (str): Key under which value is stored. + items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): Items + definition that can be converted using 'prepare_enum_items'. default (Optional[Any]): Default value. Must be one key(value) from passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. @@ -514,7 +537,12 @@ class EnumDef(AbstractAttrDef): type = "enum" def __init__( - self, key, items, default=None, multiselection=False, **kwargs + self, + key: str, + items: "Union[Dict[Any, str], List[Any], List[EnumItemDict]]", + default: "Union[str, List[Any]]" = None, + multiselection: Optional[bool] = False, + **kwargs ): if not items: raise ValueError(( @@ -525,6 +553,9 @@ class EnumDef(AbstractAttrDef): items = self.prepare_enum_items(items) item_values = [item["value"] for item in items] item_values_set = set(item_values) + if multiselection is None: + multiselection = False + if multiselection: if default is None: default = [] @@ -535,9 +566,9 @@ class EnumDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.items = items - self._item_values = item_values_set - self.multiselection = multiselection + self.items: List[EnumItemDict] = items + self._item_values: Set[Any] = item_values_set + self.multiselection: bool = multiselection def convert_value(self, value): if not self.multiselection: @@ -567,7 +598,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items): + def prepare_enum_items(items) -> List[EnumItemDict]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -583,13 +614,13 @@ class EnumDef(AbstractAttrDef): ``` Args: - items (Union[Dict[str, Any], List[Any], List[Dict[str, Any]]): The + items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): The items to convert. Returns: - List[Dict[str, Any]]: Unified structure of items. - """ + List[EnumItemDict]: Unified structure of items. + """ output = [] if isinstance(items, dict): for value, label in items.items(): @@ -644,7 +675,7 @@ class BoolDef(AbstractAttrDef): type = "bool" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[bool] = None, **kwargs): if default is None: default = False super().__init__(key, default=default, **kwargs) @@ -652,7 +683,7 @@ class BoolDef(AbstractAttrDef): def is_value_valid(self, value: Any) -> bool: return isinstance(value, bool) - def convert_value(self, value): + def convert_value(self, value: Any) -> bool: if isinstance(value, bool): return value return self.default @@ -660,7 +691,11 @@ class BoolDef(AbstractAttrDef): class FileDefItem: def __init__( - self, directory, filenames, frames=None, template=None + self, + directory: str, + filenames: List[str], + frames: Optional[List[int]] = None, + template: Optional[str] = None, ): self.directory = directory @@ -689,7 +724,7 @@ class FileDefItem: ) @property - def label(self): + def label(self) -> Optional[str]: if self.is_empty: return None @@ -732,7 +767,7 @@ class FileDefItem: filename_template, ",".join(ranges) ) - def split_sequence(self): + def split_sequence(self) -> List["Self"]: if not self.is_sequence: raise ValueError("Cannot split single file item") @@ -743,7 +778,7 @@ class FileDefItem: return self.from_paths(paths, False) @property - def ext(self): + def ext(self) -> Optional[str]: if self.is_empty: return None _, ext = os.path.splitext(self.filenames[0]) @@ -752,14 +787,14 @@ class FileDefItem: return None @property - def lower_ext(self): + def lower_ext(self) -> Optional[str]: ext = self.ext if ext is not None: return ext.lower() return ext @property - def is_dir(self): + def is_dir(self) -> bool: if self.is_empty: return False @@ -768,10 +803,15 @@ class FileDefItem: return False return True - def set_directory(self, directory): + def set_directory(self, directory: str): self.directory = directory - def set_filenames(self, filenames, frames=None, template=None): + def set_filenames( + self, + filenames: List[str], + frames: Optional[List[int]] = None, + template: Optional[str] = None, + ): if frames is None: frames = [] is_sequence = False @@ -788,11 +828,15 @@ class FileDefItem: self.is_sequence = is_sequence @classmethod - def create_empty_item(cls): + def create_empty_item(cls) -> "Self": return cls("", "") @classmethod - def from_value(cls, value, allow_sequences): + def from_value( + cls, + value: "Union[List[FileDefItemDict], FileDefItemDict]", + allow_sequences: bool, + ) -> List["Self"]: """Convert passed value to FileDefItem objects. Returns: @@ -830,7 +874,7 @@ class FileDefItem: return output @classmethod - def from_dict(cls, data): + def from_dict(cls, data: FileDefItemDict) -> "Self": return cls( data["directory"], data["filenames"], @@ -839,7 +883,11 @@ class FileDefItem: ) @classmethod - def from_paths(cls, paths, allow_sequences): + def from_paths( + cls, + paths: List[str], + allow_sequences: bool, + ) -> List["Self"]: filenames_by_dir = collections.defaultdict(list) for path in paths: normalized = os.path.normpath(path) @@ -868,7 +916,7 @@ class FileDefItem: return output - def to_dict(self): + def to_dict(self) -> FileDefItemDict: output = { "is_sequence": self.is_sequence, "directory": self.directory, @@ -906,8 +954,15 @@ class FileDef(AbstractAttrDef): ] def __init__( - self, key, single_item=True, folders=None, extensions=None, - allow_sequences=True, extensions_label=None, default=None, **kwargs + self, + key: str, + single_item: Optional[bool] = True, + folders: Optional[bool] = None, + extensions: Optional[Iterable[str]] = None, + allow_sequences: Optional[bool] = True, + extensions_label: Optional[str] = None, + default: Optional["Union[FileDefItemDict, List[str]]"] = None, + **kwargs ): if folders is None and extensions is None: folders = True @@ -943,14 +998,14 @@ class FileDef(AbstractAttrDef): if is_label_horizontal is None: kwargs["is_label_horizontal"] = False - self.single_item = single_item - self.folders = folders - self.extensions = set(extensions) - self.allow_sequences = allow_sequences - self.extensions_label = extensions_label + self.single_item: bool = single_item + self.folders: bool = folders + self.extensions: Set[str] = set(extensions) + self.allow_sequences: bool = allow_sequences + self.extensions_label: Optional[str] = extensions_label super().__init__(key, default=default, **kwargs) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if not super().__eq__(other): return False @@ -984,7 +1039,9 @@ class FileDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value( + self, value: Any + ) -> "Union[FileDefItemDict, List[FileDefItemDict]]": if isinstance(value, (str, dict)): value = [value] From 586d29f219f76572ffea9c431fe9f197cd0a2907 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:44:32 +0100 Subject: [PATCH 069/100] define 'EnumItemsInputType' for EnumDef input items --- client/ayon_core/lib/attribute_definitions.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 82c7ab9cb1..bf47b7617b 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -8,13 +8,22 @@ import warnings from abc import ABCMeta, abstractmethod import typing from typing import ( - Any, Optional, List, Set, Dict, Iterable, TypedDict, TypeVar, + Any, + Optional, + Tuple, + List, + Set, + Dict, + Iterable, + TypedDict, + TypeVar, ) import clique if typing.TYPE_CHECKING: from typing import Self, Union, Pattern + # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} @@ -28,6 +37,9 @@ class EnumItemDict(TypedDict): value: Any +EnumItemsInputType = "Union[Dict[Any, str], List[Tuple[Any, str]], List[Any], List[EnumItemDict]]" # noqa: E501 + + class FileDefItemDict(TypedDict): directory: str filenames: List[str] @@ -526,8 +538,8 @@ class EnumDef(AbstractAttrDef): Args: key (str): Key under which value is stored. - items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): Items - definition that can be converted using 'prepare_enum_items'. + items (EnumItemsInputType): Items definition that can be converted + using 'prepare_enum_items'. default (Optional[Any]): Default value. Must be one key(value) from passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. @@ -539,7 +551,7 @@ class EnumDef(AbstractAttrDef): def __init__( self, key: str, - items: "Union[Dict[Any, str], List[Any], List[EnumItemDict]]", + items: EnumItemsInputType, default: "Union[str, List[Any]]" = None, multiselection: Optional[bool] = False, **kwargs @@ -598,7 +610,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items) -> List[EnumItemDict]: + def prepare_enum_items(items: EnumItemsInputType) -> List[EnumItemDict]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -614,8 +626,7 @@ class EnumDef(AbstractAttrDef): ``` Args: - items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): The - items to convert. + items (EnumItemsInputType): The items to convert. Returns: List[EnumItemDict]: Unified structure of items. From b2a9277267a36fbbad093a5fc94b5f33e286de0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:55:48 +0100 Subject: [PATCH 070/100] define 'AttrDefType' --- client/ayon_core/lib/attribute_definitions.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index bf47b7617b..836d6c7463 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -279,6 +279,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return True +AttrDefType = TypeVar("AttrDefType", bound=AbstractAttrDef) + # ----------------------------------------- # UI attribute definitions won't hold value # ----------------------------------------- @@ -1088,13 +1090,13 @@ class FileDef(AbstractAttrDef): return [] -def register_attr_def_class(cls): +def register_attr_def_class(cls: AttrDefType): """Register attribute definition. Currently registered definitions are used to deserialize data to objects. Attrs: - cls (AbstractAttrDef): Non-abstract class to be registered with unique + cls (AttrDefType): Non-abstract class to be registered with unique 'type' attribute. Raises: @@ -1106,11 +1108,13 @@ def register_attr_def_class(cls): _attr_defs_by_type[cls.type] = cls -def get_attributes_keys(attribute_definitions): +def get_attributes_keys( + attribute_definitions: List[AttrDefType] +) -> Set[str]: """Collect keys from list of attribute definitions. Args: - attribute_definitions (List[AbstractAttrDef]): Objects of attribute + attribute_definitions (List[AttrDefType]): Objects of attribute definitions. Returns: @@ -1127,11 +1131,13 @@ def get_attributes_keys(attribute_definitions): return keys -def get_default_values(attribute_definitions): +def get_default_values( + attribute_definitions: List[AttrDefType] +) -> Dict[str, Any]: """Receive default values for attribute definitions. Args: - attribute_definitions (List[AbstractAttrDef]): Attribute definitions + attribute_definitions (List[AttrDefType]): Attribute definitions for which default values should be collected. Returns: @@ -1149,11 +1155,11 @@ def get_default_values(attribute_definitions): return output -def serialize_attr_def(attr_def): +def serialize_attr_def(attr_def: AttrDefType) -> Dict[str, Any]: """Serialize attribute definition to data. Args: - attr_def (AbstractAttrDef): Attribute definition to serialize. + attr_def (AttrDefType): Attribute definition to serialize. Returns: Dict[str, Any]: Serialized data. @@ -1162,11 +1168,13 @@ def serialize_attr_def(attr_def): return attr_def.serialize() -def serialize_attr_defs(attr_defs): +def serialize_attr_defs( + attr_defs: List[AttrDefType] +) -> List[Dict[str, Any]]: """Serialize attribute definitions to data. Args: - attr_defs (List[AbstractAttrDef]): Attribute definitions to serialize. + attr_defs (List[AttrDefType]): Attribute definitions to serialize. Returns: List[Dict[str, Any]]: Serialized data. @@ -1178,7 +1186,7 @@ def serialize_attr_defs(attr_defs): ] -def deserialize_attr_def(attr_def_data): +def deserialize_attr_def(attr_def_data: Dict[str, Any]) -> AttrDefType: """Deserialize attribute definition from data. Args: @@ -1191,7 +1199,9 @@ def deserialize_attr_def(attr_def_data): return cls.deserialize(attr_def_data) -def deserialize_attr_defs(attr_defs_data): +def deserialize_attr_defs( + attr_defs_data: List[Dict[str, Any]] +) -> List[AttrDefType]: """Deserialize attribute definitions. Args: From 341dc04cabd6982b636689cd63e83f3a9f0b3a5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:55:59 +0100 Subject: [PATCH 071/100] change formatting of docstrings --- client/ayon_core/lib/attribute_definitions.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 836d6c7463..9e1a92b18e 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -52,8 +52,8 @@ class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. Each object of `AbstractAttrDef` must have defined 'key' attribute. - """ + """ def __call__(cls, *args, **kwargs): obj = super(AbstractAttrDefMeta, cls).__call__(*args, **kwargs) init_class = getattr(obj, "__init__class__", None) @@ -116,8 +116,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): enabled (Optional[bool]): Item is enabled (for UI purposes). hidden (Optional[bool]): DEPRECATED: Use 'visible' instead. disabled (Optional[bool]): DEPRECATED: Use 'enabled' instead. - """ + """ type_attributes = [] is_value_def = True @@ -227,8 +227,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Returns: str: Type of attribute definition. - """ + """ pass @abstractmethod @@ -237,8 +237,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Convert passed value to a valid type. Use default if value can't be converted. - """ + """ pass def serialize(self) -> Dict[str, Any]: @@ -247,8 +247,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Returns: Dict[str, Any]: Serialized object that can be passed to 'deserialize' method. - """ + """ data = { "type": self.type, "key": self.key, @@ -327,8 +327,8 @@ class UnknownDef(AbstractAttrDef): This attribute can be used to keep existing data unchanged but does not have known definition of type. - """ + """ type = "unknown" def __init__(self, key: str, default: Optional[Any] = None, **kwargs): @@ -349,8 +349,8 @@ class HiddenDef(AbstractAttrDef): to other attributes (e.g. in multi-page UIs). Keep in mind the value should be possible to parse by json parser. - """ + """ type = "hidden" def __init__(self, key: str, default: Optional[Any] = None, **kwargs): @@ -376,8 +376,8 @@ class NumberDef(AbstractAttrDef): maximum(int, float): Maximum possible value. decimals(int): Maximum decimal points of value. default(int, float): Default value for conversion. - """ + """ type = "number" type_attributes = [ "minimum", @@ -466,8 +466,8 @@ class TextDef(AbstractAttrDef): regex(str, re.Pattern): Regex validation. placeholder(str): UI placeholder for attribute. default(str, None): Default value. Empty string used when not defined. - """ + """ type = "text" type_attributes = [ "multiline", @@ -546,8 +546,8 @@ class EnumDef(AbstractAttrDef): passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. Output is list of selected items. - """ + """ type = "enum" def __init__( @@ -684,8 +684,8 @@ class BoolDef(AbstractAttrDef): Args: default(bool): Default value. Set to `False` if not defined. - """ + """ type = "bool" def __init__(self, key: str, default: Optional[bool] = None, **kwargs): @@ -854,8 +854,8 @@ class FileDefItem: Returns: list: Created FileDefItem objects. - """ + """ # Convert single item to iterable if not isinstance(value, (list, tuple, set)): value = [value] @@ -1101,8 +1101,8 @@ def register_attr_def_class(cls: AttrDefType): Raises: KeyError: When type was already registered. - """ + """ if cls.type in _attr_defs_by_type: raise KeyError("Type \"{}\" was already registered".format(cls.type)) _attr_defs_by_type[cls.type] = cls @@ -1119,8 +1119,8 @@ def get_attributes_keys( Returns: Set[str]: Keys that will be created using passed attribute definitions. - """ + """ keys = set() if not attribute_definitions: return keys @@ -1142,8 +1142,8 @@ def get_default_values( Returns: Dict[str, Any]: Default values for passed attribute definitions. - """ + """ output = {} if not attribute_definitions: return output @@ -1163,8 +1163,8 @@ def serialize_attr_def(attr_def: AttrDefType) -> Dict[str, Any]: Returns: Dict[str, Any]: Serialized data. - """ + """ return attr_def.serialize() @@ -1178,8 +1178,8 @@ def serialize_attr_defs( Returns: List[Dict[str, Any]]: Serialized data. - """ + """ return [ serialize_attr_def(attr_def) for attr_def in attr_defs @@ -1192,8 +1192,8 @@ def deserialize_attr_def(attr_def_data: Dict[str, Any]) -> AttrDefType: Args: attr_def_data (Dict[str, Any]): Attribute definition data to deserialize. - """ + """ attr_type = attr_def_data.pop("type") cls = _attr_defs_by_type[attr_type] return cls.deserialize(attr_def_data) @@ -1206,8 +1206,8 @@ def deserialize_attr_defs( Args: List[Dict[str, Any]]: List of attribute definitions. - """ + """ return [ deserialize_attr_def(attr_def_data) for attr_def_data in attr_defs_data From 683bc0e39a303189149ea86f3db9747e9cb0a498 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:51:54 +0100 Subject: [PATCH 072/100] fix import --- client/ayon_core/lib/attribute_definitions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 9e1a92b18e..68c84276cb 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -10,7 +10,6 @@ import typing from typing import ( Any, Optional, - Tuple, List, Set, Dict, @@ -22,7 +21,7 @@ from typing import ( import clique if typing.TYPE_CHECKING: - from typing import Self, Union, Pattern + from typing import Tuple, Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import From 07bbe08c76e58f835c1892af63395979dcfbf26b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:08:08 +0100 Subject: [PATCH 073/100] remove 'Tuple' import Looks like the import is not needed even if the typehint is used for 'EnumItemsInputType'? --- client/ayon_core/lib/attribute_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 68c84276cb..e841a4b230 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -21,7 +21,7 @@ from typing import ( import clique if typing.TYPE_CHECKING: - from typing import Tuple, Self, Union, Pattern + from typing import Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import From 68db3d9c117df46aaf883a344aef61d26752aa22 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:48:19 +0100 Subject: [PATCH 074/100] Add logic to extract colorspace from metadata if available. - Extract colorspace from media metadata for review clips. - Update instance data with the extracted colorspace information. --- .../plugins/publish/collect_otio_review.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_otio_review.py b/client/ayon_core/plugins/publish/collect_otio_review.py index 69cf9199e7..04422391c5 100644 --- a/client/ayon_core/plugins/publish/collect_otio_review.py +++ b/client/ayon_core/plugins/publish/collect_otio_review.py @@ -95,9 +95,46 @@ class CollectOtioReview(pyblish.api.InstancePlugin): instance.data["label"] = label + " (review)" instance.data["families"] += ["review", "ftrack"] instance.data["otioReviewClips"] = otio_review_clips + self.log.info( "Creating review track: {}".format(otio_review_clips)) + # get colorspace from metadata if available + if len(otio_review_clips) >= 1 and any( + # lets make sure any clip with media reference is found + ( + clip + for clip in otio_review_clips + if isinstance(clip, otio.schema.Clip) + and clip.media_reference + ) + ): + # get metadata from first clip + # get colorspace from metadata if available + # check if resolution is the same as source + r_otio_cl = next( + ( + clip + for clip in otio_review_clips + if isinstance(clip, otio.schema.Clip) + and clip.media_reference + ), + None, + ) + + # get metadata from first clip with media reference + media_ref = r_otio_cl.media_reference + media_metadata = media_ref.metadata + + # TODO: we might need some alternative method since + # native OTIO exports do not support ayon metadata + if review_colorspace := media_metadata.get( + "ayon.source.colorspace" + ): + instance.data["reviewColorspace"] = review_colorspace + self.log.info( + "Review colorspace: {}".format(review_colorspace)) + self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) self.log.debug( From 83f28bf184bfa514294133f01787a47758ca610d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:48:30 +0100 Subject: [PATCH 075/100] Refactor plugin to include Colormanaged mixin The code changes refactor the plugin to include a Colormanaged mixin for managing colorspace data in representations. The mixin is added to the existing plugin class. --- .../plugins/publish/collect_otio_subset_resources.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 37a5e87a7a..c142036b83 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -10,12 +10,16 @@ import os import clique import pyblish.api +from ayon_core.pipeline import publish from ayon_core.pipeline.publish import ( get_publish_template_name ) -class CollectOtioSubsetResources(pyblish.api.InstancePlugin): +class CollectOtioSubsetResources( + pyblish.api.InstancePlugin, + publish.ColormanagedPyblishPluginMixin +): """Get Resources for a product version""" label = "Collect OTIO Subset Resources" @@ -190,9 +194,13 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): instance.data["originalDirname"] = self.staging_dir if repre: + colorspace = instance.data.get("colorspace") + # add colorspace data to representation + self.set_representation_colorspace( + repre, instance.context, colorspace) + # add representation to instance data instance.data["representations"].append(repre) - self.log.debug(">>>>>>>> {}".format(repre)) self.log.debug(instance.data) From 6a635b9d5e0852a77bd2bfaaa28b3ec6d1e8b4d0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:49:03 +0100 Subject: [PATCH 076/100] Update color transcoding process with debug log messages. - Add debug logs for files to convert, transcoded file, and input path. --- client/ayon_core/plugins/publish/extract_color_transcode.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 3e54d324e3..e7e0c982eb 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -5,7 +5,6 @@ import pyblish.api from ayon_core.pipeline import publish from ayon_core.lib import ( - is_oiio_supported, ) @@ -154,12 +153,15 @@ class ExtractOIIOTranscode(publish.Extractor): files_to_convert = self._translate_to_sequence( files_to_convert) + self.log.debug("Files to convert: {}".format(files_to_convert)) for file_name in files_to_convert: + self.log.debug("Transcoding file: `{}`".format(file_name)) input_path = os.path.join(original_staging_dir, file_name) output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) + self.log.debug("Ynput path: `{}`".format(input_path)) convert_colorspace( input_path, output_path, From 3a71bbca295d8d2b9d7ab452ac1b3b8f3f26037c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:49:13 +0100 Subject: [PATCH 077/100] Add colorspace data extraction to representation loop Extracts colorspace data from instance data and sets it in the representation loop for processing. --- client/ayon_core/plugins/publish/extract_colorspace_data.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_colorspace_data.py b/client/ayon_core/plugins/publish/extract_colorspace_data.py index 7da4890748..d68ad4d80d 100644 --- a/client/ayon_core/plugins/publish/extract_colorspace_data.py +++ b/client/ayon_core/plugins/publish/extract_colorspace_data.py @@ -37,6 +37,9 @@ class ExtractColorspaceData(publish.Extractor, # get colorspace settings context = instance.context + # colorspace name could be kept in instance.data + colorspace = instance.data.get("colorspace") + # loop representations for representation in representations: # skip if colorspaceData is already at representation @@ -44,5 +47,5 @@ class ExtractColorspaceData(publish.Extractor, continue self.set_representation_colorspace( - representation, context + representation, context, colorspace) ) From 0ff9ae65d8843afa0179277c2c3342fab465cec9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:50:38 +0100 Subject: [PATCH 078/100] Refactor ExtractOTIOReview class inheritance and add colorspace handling - Refactored class inheritance for ExtractOTIOReview - Added handling for colorspace data in representation creation --- .../plugins/publish/extract_otio_review.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index faba9fd36d..2c6472f8a4 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -26,7 +26,10 @@ from ayon_core.lib import ( from ayon_core.pipeline import publish -class ExtractOTIOReview(publish.Extractor): +class ExtractOTIOReview( + publish.Extractor, + publish.ColormanagedPyblishPluginMixin +): """ Extract OTIO timeline into one concuted image sequence file. @@ -78,7 +81,9 @@ class ExtractOTIOReview(publish.Extractor): self.used_frames = [] self.workfile_start = int(instance.data.get( "workfileFrameStart", 1001)) - handle_start - self.padding = len(str(self.workfile_start)) + # NOTE: padding has to be converted from + # end frame since start could be lower then 1000 + self.padding = len(str(instance.data.get("frameEnd", 1001))) self.used_frames.append(self.workfile_start) self.to_width = instance.data.get( "resolutionWidth") or self.to_width @@ -86,8 +91,10 @@ class ExtractOTIOReview(publish.Extractor): "resolutionHeight") or self.to_height # skip instance if no reviewable data available - if (not isinstance(otio_review_clips[0], otio.schema.Clip)) \ - and (len(otio_review_clips) == 1): + if ( + not isinstance(otio_review_clips[0], otio.schema.Clip) + and len(otio_review_clips) == 1 + ): self.log.warning( "Instance `{}` has nothing to process".format(instance)) return @@ -168,7 +175,7 @@ class ExtractOTIOReview(publish.Extractor): start -= clip_handle_start duration += clip_handle_start elif len(otio_review_clips) > 1 \ - and (index == len(otio_review_clips) - 1): + and (index == len(otio_review_clips) - 1): # more clips | last clip reframing with handle duration += clip_handle_end elif len(otio_review_clips) == 1: @@ -263,6 +270,13 @@ class ExtractOTIOReview(publish.Extractor): # creating and registering representation representation = self._create_representation(start, duration) + + # add colorspace data to representation + if colorspace := instance.data.get("reviewColorspace"): + self.set_representation_colorspace( + representation, instance.context, colorspace + ) + instance.data["representations"].append(representation) self.log.info("Adding representation: {}".format(representation)) From e0e541b24a01110846ff57b359a66ed8b60af81c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:57:24 +0100 Subject: [PATCH 079/100] Refactor colorspace extraction logic - Removed unnecessary closing parenthesis in colorspace extraction method. --- client/ayon_core/plugins/publish/extract_colorspace_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_colorspace_data.py b/client/ayon_core/plugins/publish/extract_colorspace_data.py index d68ad4d80d..0ffa0f3035 100644 --- a/client/ayon_core/plugins/publish/extract_colorspace_data.py +++ b/client/ayon_core/plugins/publish/extract_colorspace_data.py @@ -48,4 +48,3 @@ class ExtractColorspaceData(publish.Extractor, self.set_representation_colorspace( representation, context, colorspace) - ) From 46c6511c500804c6d690aaab02ac6c02bdf22b5d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Nov 2024 09:25:22 +0100 Subject: [PATCH 080/100] Refactor debug log in color transcoding function Removed unnecessary debug log statement from color transcoding function. --- client/ayon_core/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index e7e0c982eb..56d5d33ea4 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -161,7 +161,7 @@ class ExtractOIIOTranscode(publish.Extractor): output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) - self.log.debug("Ynput path: `{}`".format(input_path)) + convert_colorspace( input_path, output_path, From ed9b8fe430e1d5c21a3946f548b57c3f8b1d056f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:08:04 +0100 Subject: [PATCH 081/100] moved TypedDict to typecheck imports --- client/ayon_core/lib/attribute_definitions.py | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e841a4b230..02d468f1bb 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -14,14 +14,35 @@ from typing import ( Set, Dict, Iterable, - TypedDict, TypeVar, ) import clique if typing.TYPE_CHECKING: - from typing import Self, Union, Pattern + from typing import Self, Tuple, Union, TypedDict, Pattern + + + class EnumItemDict(TypedDict): + label: str + value: Any + + + EnumItemsInputType = Union[ + Dict[Any, str], + List[Tuple[Any, str]], + List[Any], + List[EnumItemDict] + ] + + + class FileDefItemDict(TypedDict): + directory: str + filenames: List[str] + frames: Optional[List[int]] + template: Optional[str] + is_sequence: Optional[bool] + # Global variable which store attribute definitions by type # - default types are registered on import @@ -31,22 +52,6 @@ _attr_defs_by_type = {} IntFloatType = "Union[int, float]" -class EnumItemDict(TypedDict): - label: str - value: Any - - -EnumItemsInputType = "Union[Dict[Any, str], List[Tuple[Any, str]], List[Any], List[EnumItemDict]]" # noqa: E501 - - -class FileDefItemDict(TypedDict): - directory: str - filenames: List[str] - frames: Optional[List[int]] - template: Optional[str] - is_sequence: Optional[bool] - - class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. @@ -552,7 +557,7 @@ class EnumDef(AbstractAttrDef): def __init__( self, key: str, - items: EnumItemsInputType, + items: "EnumItemsInputType", default: "Union[str, List[Any]]" = None, multiselection: Optional[bool] = False, **kwargs @@ -579,7 +584,7 @@ class EnumDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.items: List[EnumItemDict] = items + self.items: List["EnumItemDict"] = items self._item_values: Set[Any] = item_values_set self.multiselection: bool = multiselection @@ -611,7 +616,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items: EnumItemsInputType) -> List[EnumItemDict]: + def prepare_enum_items(items: "EnumItemsInputType") -> List["EnumItemDict"]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -886,7 +891,7 @@ class FileDefItem: return output @classmethod - def from_dict(cls, data: FileDefItemDict) -> "Self": + def from_dict(cls, data: "FileDefItemDict") -> "Self": return cls( data["directory"], data["filenames"], @@ -928,7 +933,7 @@ class FileDefItem: return output - def to_dict(self) -> FileDefItemDict: + def to_dict(self) -> "FileDefItemDict": output = { "is_sequence": self.is_sequence, "directory": self.directory, From ad25aa7b525276da52bf80e2686a0482954a1bec Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Thu, 7 Nov 2024 12:47:24 +0100 Subject: [PATCH 082/100] Use open -R for opening explorer on MacOS --- client/ayon_core/plugins/actions/open_file_explorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index 50a3107444..e96392ec00 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -99,7 +99,7 @@ class OpenTaskPath(LauncherAction): if platform_name == "windows": args = ["start", path] elif platform_name == "darwin": - args = ["open", "-na", path] + args = ["open", "-R", path] elif platform_name == "linux": args = ["xdg-open", path] else: From 7d23e1ac3fc29a8e8bc99ef94283f62f2b9f746f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Nov 2024 16:36:47 +0100 Subject: [PATCH 083/100] Fix support for scriptsmenu running commands in Qt6 (e.g. PySide6 in Maya 2025) --- client/ayon_core/vendor/python/scriptsmenu/action.py | 9 +++++---- .../ayon_core/vendor/python/scriptsmenu/launchformaya.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/vendor/python/scriptsmenu/action.py b/client/ayon_core/vendor/python/scriptsmenu/action.py index 49b08788f9..3ba281fed7 100644 --- a/client/ayon_core/vendor/python/scriptsmenu/action.py +++ b/client/ayon_core/vendor/python/scriptsmenu/action.py @@ -1,6 +1,6 @@ import os -from qtpy import QtWidgets +from qtpy import QtWidgets, QT6 class Action(QtWidgets.QAction): @@ -112,20 +112,21 @@ module.{module_name}()""" Run the command of the instance or copy the command to the active shelf based on the current modifiers. - If callbacks have been registered with fouind modifier integer the + If callbacks have been registered with found modifier integer the function will trigger all callbacks. When a callback function returns a non zero integer it will not execute the action's command - """ # get the current application and its linked keyboard modifiers app = QtWidgets.QApplication.instance() modifiers = app.keyboardModifiers() + if not QT6: + modifiers = int(modifiers) # If the menu has a callback registered for the current modifier # we run the callback instead of the action itself. registered = self._root.registered_callbacks - callbacks = registered.get(int(modifiers), []) + callbacks = registered.get(modifiers, []) for callback in callbacks: signal = callback(self) if signal != 0: diff --git a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py index 496278ac6f..a5503bc63e 100644 --- a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py +++ b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py @@ -4,7 +4,7 @@ import maya.cmds as cmds import maya.mel as mel import scriptsmenu -from qtpy import QtCore, QtWidgets +from qtpy import QtCore, QtWidgets, QT6 log = logging.getLogger(__name__) @@ -130,7 +130,7 @@ def main(title="Scripts", parent=None, objectName=None): # Register control + shift callback to add to shelf (maya behavior) modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier - if int(cmds.about(version=True)) < 2025: + if not QT6: modifiers = int(modifiers) menu.register_callback(modifiers, to_shelf) From 2337d116d54eaabfd73b81f9f45c1865e124a65a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:46:53 +0100 Subject: [PATCH 084/100] change is_latest based on version item --- client/ayon_core/tools/sceneinventory/model.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index b7f79986ac..9b1e75a0d1 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -194,14 +194,14 @@ class InventoryModel(QtGui.QStandardItemModel): group_items = [] for repre_id, container_items in items_by_repre_id.items(): repre_info = repre_info_by_id[repre_id] - version_label = "N/A" version_color = None - is_latest = False - is_hero = False - status_name = None if not repre_info.is_valid: + version_label = "N/A" group_name = "< Entity N/A >" item_icon = invalid_item_icon + is_latest = False + is_hero = False + status_name = None else: group_name = "{}_{}: ({})".format( @@ -217,6 +217,7 @@ class InventoryModel(QtGui.QStandardItemModel): version_item = version_items[repre_info.version_id] version_label = format_version(version_item.version) is_hero = version_item.version < 0 + is_latest = version_item.is_latest if not version_item.is_latest: version_color = self.OUTDATED_COLOR status_name = version_item.status From 8a7239fc0511c19bfb1f1a0bf0e01d18b9026fa8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:47:04 +0100 Subject: [PATCH 085/100] remove unncessary line --- client/ayon_core/tools/sceneinventory/models/containers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 871455c96b..4f3ddf1ded 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -383,7 +383,6 @@ class ContainersModel: container_items_by_id[item.item_id] = item container_items.append(item) - self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id self._items_cache = container_items From 1de069c324a8d49c7e1424fe7d0fd95539238145 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:47:15 +0100 Subject: [PATCH 086/100] remove unnessary conversion --- client/ayon_core/tools/sceneinventory/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 9b1e75a0d1..bdcd183c99 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -426,7 +426,7 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): state = bool(state) if state != self._filter_outdated: - self._filter_outdated = bool(state) + self._filter_outdated = state self.invalidateFilter() def set_hierarchy_view(self, state): From 749984c0bff74c4491a7d6e853afa70906b1e984 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 8 Nov 2024 13:10:44 +0100 Subject: [PATCH 087/100] Fix loader load option box widgets --- client/ayon_core/tools/attribute_defs/widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 22f4bfe535..93f63730f5 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -202,7 +202,6 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): self._current_keys.add(attr_def.key) widget = create_widget_for_attr_def(attr_def, self) - self._widgets.append(widget) self._widgets_by_id[attr_def.id] = widget if not attr_def.visible: From 20206a3cf3444c3a74b8f3aa046985def1cbfa38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:57:00 +0100 Subject: [PATCH 088/100] check executable name before killing the process --- client/ayon_core/tools/tray/lib.py | 103 ++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 39fcc2cdd3..94550775e6 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -3,12 +3,9 @@ import sys import json import hashlib import platform -import subprocess -import csv import time import signal -import locale -from typing import Optional, Dict, Tuple, Any +from typing import Optional, List, Dict, Tuple, Any import requests from ayon_api.utils import get_default_settings_variant @@ -53,15 +50,99 @@ def _get_server_and_variant( return server_url, variant +def _windows_get_pid_args(pid: int) -> Optional[List[str]]: + import ctypes + from ctypes import wintypes + + # Define constants + PROCESS_COMMANDLINE_INFO = 60 + STATUS_NOT_FOUND = 0xC0000225 + PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + + # Define the UNICODE_STRING structure + class UNICODE_STRING(ctypes.Structure): + _fields_ = [ + ("Length", wintypes.USHORT), + ("MaximumLength", wintypes.USHORT), + ("Buffer", wintypes.LPWSTR) + ] + + shell32 = ctypes.WinDLL("shell32", use_last_error=True) + + CommandLineToArgvW = shell32.CommandLineToArgvW + CommandLineToArgvW.argtypes = [ + wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int) + ] + CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR) + + output = None + # Open the process + handle = ctypes.windll.kernel32.OpenProcess( + PROCESS_QUERY_LIMITED_INFORMATION, False, pid + ) + if not handle: + return output + + try: + buffer_len = wintypes.ULONG() + # Get the right buffer size first + status = ctypes.windll.ntdll.NtQueryInformationProcess( + handle, + PROCESS_COMMANDLINE_INFO, + ctypes.c_void_p(None), + 0, + ctypes.byref(buffer_len) + ) + + if status == STATUS_NOT_FOUND: + return output + + # Create buffer with collected size + buffer = ctypes.create_string_buffer(buffer_len.value) + + # Get the command line + status = ctypes.windll.ntdll.NtQueryInformationProcess( + handle, + PROCESS_COMMANDLINE_INFO, + buffer, + buffer_len, + ctypes.byref(buffer_len) + ) + if status: + return output + # Build the string + tmp = ctypes.cast(buffer, ctypes.POINTER(UNICODE_STRING)).contents + size = tmp.Length // 2 + 1 + cmdline_buffer = ctypes.create_unicode_buffer(size) + ctypes.cdll.msvcrt.wcscpy(cmdline_buffer, tmp.Buffer) + + args_len = ctypes.c_int() + args = CommandLineToArgvW( + cmdline_buffer, ctypes.byref(args_len) + ) + output = [args[idx] for idx in range(args_len.value)] + ctypes.windll.kernel32.LocalFree(args) + + finally: + ctypes.windll.kernel32.CloseHandle(handle) + return output def _windows_pid_is_running(pid: int) -> bool: - args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"] - output = subprocess.check_output(args) - encoding = locale.getpreferredencoding() - csv_content = csv.DictReader(output.decode(encoding).splitlines()) - # if "PID" not in csv_content.fieldnames: - # return False - for _ in csv_content: + args = _windows_get_pid_args(pid) + if not args: + return False + executable_path = args[0] + + filename = os.path.basename(executable_path).lower() + if "ayon" in filename: return True + + # Try to handle tray running from code + # - this might be potential danger that kills other python process running + # 'start.py' script (low chance, but still) + if "python" in filename and len(args) > 1: + script_filename = os.path.basename(args[1].lower()) + if script_filename == "start.py": + return True return False From 41db386f23f4ec18e870abe1817d0f71eb8fc775 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:59:05 +0100 Subject: [PATCH 089/100] add empty lines --- client/ayon_core/tools/tray/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 94550775e6..13ee1eea5c 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -126,6 +126,8 @@ def _windows_get_pid_args(pid: int) -> Optional[List[str]]: finally: ctypes.windll.kernel32.CloseHandle(handle) return output + + def _windows_pid_is_running(pid: int) -> bool: args = _windows_get_pid_args(pid) if not args: From 262cc0e7bb117516b1c1a3a7ef3b71d0508c8adf Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 8 Nov 2024 18:07:10 +0000 Subject: [PATCH 090/100] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 2b2af81e18..74f64e7944 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.6+dev" +__version__ = "1.0.7" diff --git a/package.py b/package.py index 59f0e82be0..c3fc02b625 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.6+dev" +version = "1.0.7" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index ca626eff00..12a68630e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.6+dev" +version = "1.0.7" description = "" authors = ["Ynput Team "] readme = "README.md" From 7ae9b1815378352ed86f7b0dee251d58995bf11a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 8 Nov 2024 18:07:45 +0000 Subject: [PATCH 091/100] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 74f64e7944..3a5b63785d 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.7" +__version__ = "1.0.7+dev" diff --git a/package.py b/package.py index c3fc02b625..ef2f3822eb 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.7" +version = "1.0.7+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 12a68630e2..78a3021b30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.7" +version = "1.0.7+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From a7908a46e94cf22ca7b42fa5f2dab2657a3e8f13 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Nov 2024 12:50:05 +0100 Subject: [PATCH 092/100] Update handling of missing otioReviewClips data - Handle case where otioReviewClips is missing by logging a message. --- client/ayon_core/plugins/publish/extract_otio_review.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 2c6472f8a4..b222c6efc3 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -74,7 +74,10 @@ class ExtractOTIOReview( # TODO: what if handles are different in `versionData`? handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] - otio_review_clips = instance.data["otioReviewClips"] + otio_review_clips = instance.data.get("otioReviewClips") + + if otio_review_clips is None: + self.log.info(f"Instance `{instance}` has no otioReviewClips") # add plugin wide attributes self.representation_files = [] From 3da898b3440b5fd9ba75887205e033cc834685a2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 11 Nov 2024 15:37:59 +0100 Subject: [PATCH 093/100] Update client/ayon_core/pipeline/publish/publish_plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/publish/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 6a2f4c0279..57215eff68 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -206,7 +206,7 @@ class AYONPyblishPluginMixin: return False families = [instance.product_type] - families.extend(instance.data.get("families", [])) + families.extend(instance.get("families", [])) for _ in pyblish.logic.plugins_by_families([cls], families): return True return False From a6729802dc6c0f1cd26dbf9447536d947797f906 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:58:47 +0100 Subject: [PATCH 094/100] make sure version combobox has no focus policy --- client/ayon_core/tools/loader/ui/products_delegates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 9753da37af..fba9b5b3ca 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -222,6 +222,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): editor = VersionComboBox(product_id, parent) editor.setProperty("itemId", item_id) + editor.setFocusPolicy(QtCore.Qt.NoFocus) editor.value_changed.connect(self._on_editor_change) editor.destroyed.connect(self._on_destroy) From a089b17f2ffc673830520d95976a57148905a965 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:20:21 +0100 Subject: [PATCH 095/100] added '__required_keys' to CreatedInstance --- client/ayon_core/pipeline/create/structures.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index ba4a373597..fdd41b7255 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -434,6 +434,13 @@ class CreatedInstance: "creator_attributes", "publish_attributes" ) + # Keys that can be changed, but should not be removed from instance + __required_keys = { + "folderPath": None, + "task": None, + "productName": None, + "active": True, + } def __init__( self, @@ -515,6 +522,9 @@ class CreatedInstance: if data: self._data.update(data) + for key, default in self.__required_keys.items(): + self._data.setdefault(key, default) + if not self._data.get("instance_id"): self._data["instance_id"] = str(uuid4()) @@ -567,6 +577,8 @@ class CreatedInstance: has_key = key in self._data output = self._data.pop(key, *args, **kwargs) if has_key: + if key in self.__required_keys: + self._data[key] = self.__required_keys[key] self._create_context.instance_values_changed( self.id, {key: None} ) From 2cf62f0bb455c401897f85f641774e70770ca1fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:20:35 +0100 Subject: [PATCH 096/100] fix product type key in immutable keys --- client/ayon_core/pipeline/create/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index fdd41b7255..a1a4d5f8ef 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -429,7 +429,7 @@ class CreatedInstance: __immutable_keys = ( "id", "instance_id", - "product_type", + "productType", "creator_identifier", "creator_attributes", "publish_attributes" From b79e0189a073b579f9e45a0496ec6f87cbb3a617 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:43:17 +0100 Subject: [PATCH 097/100] Use N/A label if is not available --- client/ayon_core/tools/publisher/models/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index ca26749b65..9644af43e0 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -296,7 +296,7 @@ class InstanceItem: return InstanceItem( instance.id, instance.creator_identifier, - instance.label, + instance.label or "N/A", instance.group_label, instance.product_type, instance.product_name, From 235949b867aff1f5caf5139e60ec4ca136ae2d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 12 Nov 2024 15:58:08 +0100 Subject: [PATCH 098/100] Update client/ayon_core/plugins/publish/collect_otio_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/collect_otio_review.py | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_review.py b/client/ayon_core/plugins/publish/collect_otio_review.py index 04422391c5..4708b0a97c 100644 --- a/client/ayon_core/plugins/publish/collect_otio_review.py +++ b/client/ayon_core/plugins/publish/collect_otio_review.py @@ -100,37 +100,33 @@ class CollectOtioReview(pyblish.api.InstancePlugin): "Creating review track: {}".format(otio_review_clips)) # get colorspace from metadata if available - if len(otio_review_clips) >= 1 and any( - # lets make sure any clip with media reference is found + # get metadata from first clip with media reference + r_otio_cl = next( ( clip for clip in otio_review_clips - if isinstance(clip, otio.schema.Clip) - and clip.media_reference - ) - ): - # get metadata from first clip - # get colorspace from metadata if available - # check if resolution is the same as source - r_otio_cl = next( - ( - clip - for clip in otio_review_clips - if isinstance(clip, otio.schema.Clip) + if ( + isinstance(clip, otio.schema.Clip) and clip.media_reference - ), - None, - ) - - # get metadata from first clip with media reference + ) + ), + None + ) + if r_otio_cl is not None: media_ref = r_otio_cl.media_reference media_metadata = media_ref.metadata # TODO: we might need some alternative method since # native OTIO exports do not support ayon metadata - if review_colorspace := media_metadata.get( + review_colorspace = media_metadata.get( "ayon.source.colorspace" - ): + ) + if review_colorspace is None: + # Backwards compatibility for older scenes + review_colorspace = media_metadata.get( + "openpype.source.colourtransform" + ) + if review_colorspace: instance.data["reviewColorspace"] = review_colorspace self.log.info( "Review colorspace: {}".format(review_colorspace)) From bccd8d813c7b22c57abd3b8b25ad6d19e55a6911 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 13 Nov 2024 10:51:16 +0000 Subject: [PATCH 099/100] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 3a5b63785d..7702eb67ad 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.7+dev" +__version__ = "1.0.8" diff --git a/package.py b/package.py index ef2f3822eb..bd61438898 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.7+dev" +version = "1.0.8" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 78a3021b30..236a7ddc6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.7+dev" +version = "1.0.8" description = "" authors = ["Ynput Team "] readme = "README.md" From a181fc897d16db9563fca73473eddee590cdd427 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 13 Nov 2024 10:51:57 +0000 Subject: [PATCH 100/100] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 7702eb67ad..63f7de04dc 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.8" +__version__ = "1.0.8+dev" diff --git a/package.py b/package.py index bd61438898..bbfcc51019 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.8" +version = "1.0.8+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 236a7ddc6c..e29aa08c6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.8" +version = "1.0.8+dev" description = "" authors = ["Ynput Team "] readme = "README.md"