From 66353ec8c4f0d81d29c7b1ef7fd997e07bf754ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:29:48 +0200 Subject: [PATCH 1/8] change how context validation happens on instances --- client/ayon_core/pipeline/create/context.py | 127 +++++++++++++----- .../ayon_core/pipeline/create/structures.py | 76 +++++------ client/ayon_core/tools/publisher/abstract.py | 6 + client/ayon_core/tools/publisher/control.py | 3 + .../tools/publisher/models/create.py | 8 ++ .../publisher/widgets/card_view_widgets.py | 44 +++--- .../publisher/widgets/list_view_widgets.py | 29 ++-- .../tools/publisher/widgets/widgets.py | 26 +++- client/ayon_core/tools/publisher/window.py | 16 ++- 9 files changed, 213 insertions(+), 122 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3f067427fa..7706860499 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -6,7 +6,8 @@ import traceback import collections import inspect from contextlib import contextmanager -from typing import Optional +import typing +from typing import Optional, Iterable, Dict import pyblish.logic import pyblish.api @@ -31,13 +32,15 @@ from .exceptions import ( HostMissRequiredMethod, ) from .changes import TrackChangesItem -from .structures import PublishAttributes, ConvertorItem +from .structures import PublishAttributes, ConvertorItem, InstanceContextInfo from .creator_plugins import ( Creator, AutoCreator, discover_creator_plugins, discover_convertor_plugins, ) +if typing.TYPE_CHECKING: + from .structures import CreatedInstance # Import of functions and classes that were moved to different file # TODO Should be removed in future release - Added 24/08/28, 0.4.3-dev.1 @@ -183,6 +186,10 @@ class CreateContext: # Shared data across creators during collection phase self._collection_shared_data = None + # Context validation cache + self._folder_id_by_folder_path = {} + self._task_names_by_folder_path = {} + self.thumbnail_paths_by_instance_id = {} # Trigger reset if was enabled @@ -202,17 +209,19 @@ class CreateContext: """Access to global publish attributes.""" return self._publish_attributes - def get_instance_by_id(self, instance_id): + def get_instance_by_id( + self, instance_id: str + ) -> Optional["CreatedInstance"]: """Receive instance by id. Args: instance_id (str): Instance id. Returns: - Union[CreatedInstance, None]: Instance or None if instance with + Optional[CreatedInstance]: Instance or None if instance with given id is not available. - """ + """ return self._instances_by_id.get(instance_id) def get_sorted_creators(self, identifiers=None): @@ -224,8 +233,8 @@ class CreateContext: Returns: List[BaseCreator]: Sorted creator plugins by 'order' value. - """ + """ if identifiers is not None: identifiers = set(identifiers) creators = [ @@ -491,6 +500,8 @@ 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 = {} def reset_finalization(self): """Cleanup of attributes after reset.""" @@ -715,7 +726,7 @@ class CreateContext: self._original_context_data, self.context_data_to_store() ) - def creator_adds_instance(self, instance): + def creator_adds_instance(self, instance: "CreatedInstance"): """Creator adds new instance to context. Instances should be added only from creators. @@ -942,7 +953,7 @@ class CreateContext: def _remove_instance(self, instance): self._instances_by_id.pop(instance.id, None) - def creator_removed_instance(self, instance): + def creator_removed_instance(self, instance: "CreatedInstance"): """When creator removes instance context should be acknowledged. If creator removes instance conext should know about it to avoid @@ -990,7 +1001,7 @@ class CreateContext: [], self._bulk_instances_to_process ) - self.validate_instances_context(instances_to_validate) + self.get_instances_context_info(instances_to_validate) def reset_instances(self): """Reload instances""" @@ -1079,26 +1090,70 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) - def validate_instances_context(self, instances=None): - """Validate 'folder' and 'task' instance context.""" + def get_instances_context_info( + self, instances: Optional[Iterable["CreatedInstance"]] = None + ) -> Dict[str, InstanceContextInfo]: + """Validate 'folder' and 'task' instance context. + + Args: + instances (Optional[Iterable[CreatedInstance]]): Instances to + validate. If not provided all instances are validated. + + Returns: + Dict[str, InstanceContextInfo]: Validation results by instance id. + + """ # Use all instances from context if 'instances' are not passed if instances is None: - instances = tuple(self._instances_by_id.values()) + instances = self._instances_by_id.values() + instances = tuple(instances) + info_by_instance_id = { + instance.id: InstanceContextInfo( + instance.get("folderPath"), + instance.get("task"), + False, + False, + ) + for instance in instances + } # Skip if instances are empty - if not instances: - return + if not info_by_instance_id: + return info_by_instance_id project_name = self.project_name - task_names_by_folder_path = {} + to_validate = [] + task_names_by_folder_path = collections.defaultdict(set) for instance in instances: - folder_path = instance.get("folderPath") - task_name = instance.get("task") - if folder_path: - task_names_by_folder_path[folder_path] = set() - if task_name: - task_names_by_folder_path[folder_path].add(task_name) + context_info = info_by_instance_id[instance.id] + if instance.has_promised_context: + context_info.folder_is_valid = True + context_info.task_is_valid = True + continue + # TODO allow context promise + folder_path = context_info.folder_path + 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: + continue + context_info.folder_is_valid = True + + task_name = context_info.task_name + if task_name is not None: + 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 + + to_validate.append(instance) + task_names_by_folder_path[folder_path].add(task_name) + + if not to_validate: + return info_by_instance_id # Backwards compatibility for cases where folder name is set instead # of folder path @@ -1120,7 +1175,9 @@ class CreateContext: fields={"id", "path"} ): folder_id = folder_entity["id"] - folder_paths_by_id[folder_id] = folder_entity["path"] + 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: @@ -1131,8 +1188,10 @@ class CreateContext: ): folder_id = folder_entity["id"] folder_name = folder_entity["name"] - folder_paths_by_id[folder_id] = folder_entity["path"] + 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 tasks_entities = ayon_api.get_tasks( project_name, @@ -1145,12 +1204,11 @@ class CreateContext: 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 instances: - if not instance.has_valid_folder or not instance.has_valid_task: - continue - + 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: @@ -1158,15 +1216,16 @@ class CreateContext: instance["folderPath"] = folder_path if folder_path not in task_names_by_folder_path: - instance.set_folder_invalid(True) continue + context_info = info_by_instance_id[instance.id] + context_info.folder_is_valid = True - task_name = instance["task"] - if not task_name: - continue - - if task_name not in task_names_by_folder_path[folder_path]: - instance.set_task_invalid(True) + if ( + not task_name + or task_name in task_names_by_folder_path[folder_path] + ): + context_info.task_is_valid = True + return info_by_instance_id def save_changes(self): """Save changes. Update all changed values.""" diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 4f7caa6e11..311d382ac9 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -1,6 +1,7 @@ import copy import collections from uuid import uuid4 +from typing import Optional from ayon_core.lib.attribute_definitions import ( UnknownDef, @@ -396,6 +397,24 @@ class PublishAttributes: ) +class InstanceContextInfo: + def __init__( + self, + folder_path: Optional[str], + task_name: Optional[str], + folder_is_valid: bool, + task_is_valid: bool, + ): + self.folder_path: Optional[str] = folder_path + self.task_name: Optional[str] = task_name + self.folder_is_valid: bool = folder_is_valid + self.task_is_valid: bool = task_is_valid + + @property + def is_valid(self) -> bool: + return self.folder_is_valid and self.task_is_valid + + class CreatedInstance: """Instance entity with data that will be stored to workfile. @@ -528,9 +547,6 @@ class CreatedInstance: if not self._data.get("instance_id"): self._data["instance_id"] = str(uuid4()) - self._folder_is_valid = self.has_set_folder - self._task_is_valid = self.has_set_task - def __str__(self): return ( " bool: + """Get context data that are promised to be set by creator. + + Returns: + bool: Has context that won't bo validated. Artist can't change + value when set to True. + + """ + return self._data.get("has_promised_context", False) + def data_to_store(self): """Collect data that contain json parsable types. @@ -826,46 +853,3 @@ class CreatedInstance: obj.publish_attributes.deserialize_attributes(publish_attributes) return obj - - # Context validation related methods/properties - @property - def has_set_folder(self): - """Folder path is set in data.""" - - return "folderPath" in self._data - - @property - def has_set_task(self): - """Task name is set in data.""" - - return "task" in self._data - - @property - def has_valid_context(self): - """Context data are valid for publishing.""" - - return self.has_valid_folder and self.has_valid_task - - @property - def has_valid_folder(self): - """Folder set in context exists in project.""" - - if not self.has_set_folder: - return False - return self._folder_is_valid - - @property - def has_valid_task(self): - """Task set in context exists in project.""" - - if not self.has_set_task: - return False - return self._task_is_valid - - def set_folder_invalid(self, invalid): - # TODO replace with `set_folder_path` - self._folder_is_valid = not invalid - - def set_task_invalid(self, invalid): - # TODO replace with `set_task_name` - self._task_is_valid = not invalid diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 362fa38882..ad566eb354 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -322,6 +322,12 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ) -> Dict[str, Union[CreatedInstance, None]]: pass + @abstractmethod + def get_instances_context_info( + self, instance_ids: Optional[Iterable[str]] = None + ): + pass + @abstractmethod def get_existing_product_names(self, folder_path: str) -> List[str]: pass diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 257b45de08..fe1545f219 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -190,6 +190,9 @@ class PublisherController( def get_instances_by_id(self, instance_ids=None): return self._create_model.get_instances_by_id(instance_ids) + def get_instances_context_info(self, instance_ids=None): + return self._create_model.get_instances_context_info(instance_ids) + def get_convertor_items(self): return self._create_model.get_convertor_items() diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 9fe114f4bd..dcd2ce4acc 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -306,6 +306,14 @@ class CreateModel: for instance_id in instance_ids } + def get_instances_context_info( + self, instance_ids: Optional[Iterable[str]] = None + ): + instances = self.get_instances_by_id(instance_ids).values() + return self._create_context.get_instances_context_info( + instances + ) + def get_convertor_items(self) -> Dict[str, ConvertorItem]: return self._create_context.convertor_items_by_id diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index d67252e302..c0e27d9c60 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -217,20 +217,22 @@ class InstanceGroupWidget(BaseGroupWidget): def update_icons(self, group_icons): self._group_icons = group_icons - def update_instance_values(self): + def update_instance_values(self, context_info_by_id): """Trigger update on instance widgets.""" - for widget in self._widgets_by_id.values(): - widget.update_instance_values() + for instance_id, widget in self._widgets_by_id.items(): + widget.update_instance_values(context_info_by_id[instance_id]) - def update_instances(self, instances): + def update_instances(self, instances, context_info_by_id): """Update instances for the group. Args: - instances(list): List of instances in + instances (list[CreatedInstance]): List of instances in CreateContext. - """ + context_info_by_id (Dict[str, InstanceContextInfo]): Instance + context info by instance id. + """ # Store instances by id and by product name instances_by_id = {} instances_by_product_name = collections.defaultdict(list) @@ -249,13 +251,14 @@ class InstanceGroupWidget(BaseGroupWidget): widget_idx = 1 for product_names in sorted_product_names: for instance in instances_by_product_name[product_names]: + context_info = context_info_by_id[instance.id] if instance.id in self._widgets_by_id: widget = self._widgets_by_id[instance.id] - widget.update_instance(instance) + widget.update_instance(instance, context_info) else: group_icon = self._group_icons[instance.creator_identifier] widget = InstanceCardWidget( - instance, group_icon, self + instance, context_info, group_icon, self ) widget.selected.connect(self._on_widget_selection) widget.active_changed.connect(self._on_active_changed) @@ -388,7 +391,7 @@ class ConvertorItemCardWidget(CardWidget): self._icon_widget = icon_widget self._label_widget = label_widget - def update_instance_values(self): + def update_instance_values(self, context_info): pass @@ -397,7 +400,7 @@ class InstanceCardWidget(CardWidget): active_changed = QtCore.Signal(str, bool) - def __init__(self, instance, group_icon, parent): + def __init__(self, instance, context_info, group_icon, parent): super().__init__(parent) self._id = instance.id @@ -458,7 +461,7 @@ class InstanceCardWidget(CardWidget): self._active_checkbox = active_checkbox self._expand_btn = expand_btn - self.update_instance_values() + self.update_instance_values(context_info) def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) @@ -480,13 +483,13 @@ class InstanceCardWidget(CardWidget): if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) - def update_instance(self, instance): + def update_instance(self, instance, context_info): """Update instance object and update UI.""" self.instance = instance - self.update_instance_values() + self.update_instance_values(context_info) - def _validate_context(self): - valid = self.instance.has_valid_context + def _validate_context(self, context_info): + valid = context_info.is_valid self._icon_widget.setVisible(valid) self._context_warning.setVisible(not valid) @@ -519,11 +522,11 @@ class InstanceCardWidget(CardWidget): QtCore.Qt.NoTextInteraction ) - def update_instance_values(self): + def update_instance_values(self, context_info): """Update instance data""" self._update_product_name() self.set_active(self.instance["active"]) - self._validate_context() + self._validate_context(context_info) def _set_expanded(self, expanded=None): if expanded is None: @@ -694,6 +697,8 @@ class InstanceCardView(AbstractInstanceView): self._update_convertor_items_group() + context_info_by_id = self._controller.get_instances_context_info() + # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) identifiers_by_group = collections.defaultdict(set) @@ -747,7 +752,7 @@ class InstanceCardView(AbstractInstanceView): widget_idx += 1 group_widget.update_instances( - instances_by_group[group_name] + instances_by_group[group_name], context_info_by_id ) group_widget.set_active_toggle_enabled( self._active_toggle_enabled @@ -814,8 +819,9 @@ class InstanceCardView(AbstractInstanceView): def refresh_instance_states(self): """Trigger update of instances on group widgets.""" + context_info_by_id = self._controller.get_instances_context_info() for widget in self._widgets_by_group.values(): - widget.update_instance_values() + widget.update_instance_values(context_info_by_id) def _on_active_changed(self, group_name, instance_id, value): group_widget = self._widgets_by_group[group_name] diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index 930d6bb88c..ab9f2db52c 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -115,7 +115,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): active_changed = QtCore.Signal(str, bool) double_clicked = QtCore.Signal() - def __init__(self, instance, parent): + def __init__(self, instance, context_info, parent): super().__init__(parent) self.instance = instance @@ -151,7 +151,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): self._has_valid_context = None - self._set_valid_property(instance.has_valid_context) + self._set_valid_property(context_info.is_valid) def mouseDoubleClickEvent(self, event): widget = self.childAt(event.pos()) @@ -188,12 +188,12 @@ class InstanceListItemWidget(QtWidgets.QWidget): if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) - def update_instance(self, instance): + def update_instance(self, instance, context_info): """Update instance object.""" self.instance = instance - self.update_instance_values() + self.update_instance_values(context_info) - def update_instance_values(self): + def update_instance_values(self, context_info): """Update instance data propagated to widgets.""" # Check product name label = self.instance.label @@ -202,7 +202,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): # Check active state self.set_active(self.instance["active"]) # Check valid states - self._set_valid_property(self.instance.has_valid_context) + self._set_valid_property(context_info.is_valid) def _on_active_change(self): new_value = self._active_checkbox.isChecked() @@ -583,6 +583,8 @@ class InstanceListView(AbstractInstanceView): self._update_convertor_items_group() + context_info_by_id = self._controller.get_instances_context_info() + # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() @@ -643,13 +645,15 @@ class InstanceListView(AbstractInstanceView): elif activity != instance["active"]: activity = -1 + context_info = context_info_by_id[instance_id] + self._group_by_instance_id[instance_id] = group_name # Remove instance id from `to_remove` if already exists and # trigger update of widget if instance_id in to_remove: to_remove.remove(instance_id) widget = self._widgets_by_id[instance_id] - widget.update_instance(instance) + widget.update_instance(instance, context_info) continue # Create new item and store it as new @@ -695,7 +699,8 @@ class InstanceListView(AbstractInstanceView): group_item.appendRows(new_items) for item, instance in new_items_with_instance: - if not instance.has_valid_context: + context_info = context_info_by_id[instance.id] + if not context_info.is_valid: expand_groups.add(group_name) item_index = self._instance_model.index( item.row(), @@ -704,7 +709,7 @@ class InstanceListView(AbstractInstanceView): ) proxy_index = self._proxy_model.mapFromSource(item_index) widget = InstanceListItemWidget( - instance, self._instance_view + instance, context_info, self._instance_view ) widget.set_active_toggle_enabled( self._active_toggle_enabled @@ -870,8 +875,10 @@ class InstanceListView(AbstractInstanceView): def refresh_instance_states(self): """Trigger update of all instances.""" - for widget in self._widgets_by_id.values(): - widget.update_instance_values() + context_info_by_id = self._controller.get_instances_context_info() + for instance_id, widget in self._widgets_by_id.items(): + context_info = context_info_by_id[instance_id] + widget.update_instance_values(context_info) def _on_active_changed(self, changed_instance_id, new_value): selected_instance_ids, _, _ = self.get_selected_items() diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 1f782ddc67..4ad527126b 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1206,7 +1206,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget): except TaskNotSetError: invalid_tasks = True - instance.set_task_invalid(True) product_names.add(instance["productName"]) continue @@ -1216,11 +1215,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if folder_path is not None: instance["folderPath"] = folder_path - instance.set_folder_invalid(False) if task_name is not None: instance["task"] = task_name or None - instance.set_task_invalid(False) instance["productName"] = new_product_name @@ -1768,9 +1765,16 @@ class ProductAttributesWidget(QtWidgets.QWidget): self.bottom_separator = bottom_separator def _on_instance_context_changed(self): + instance_ids = { + instance.id + for instance in self._current_instances + } + context_info_by_id = self._controller.get_instances_context_info( + instance_ids + ) all_valid = True - for instance in self._current_instances: - if not instance.has_valid_context: + for instance_id, context_info in context_info_by_id.items(): + if not context_info.is_valid: all_valid = False break @@ -1795,9 +1799,17 @@ class ProductAttributesWidget(QtWidgets.QWidget): convertor_identifiers(List[str]): Identifiers of convert items. """ + instance_ids = { + instance.id + for instance in instances + } + context_info_by_id = self._controller.get_instances_context_info( + instance_ids + ) + all_valid = True - for instance in instances: - if not instance.has_valid_context: + for context_info in context_info_by_id.values(): + if not context_info.is_valid: all_valid = False break diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 0c6087b41d..a8ca605ecb 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -913,12 +913,18 @@ class PublisherWindow(QtWidgets.QDialog): self._set_footer_enabled(True) return + active_instances_by_id = { + instance.id: instance + for instance in self._controller.get_instances() + if instance["active"] + } + context_info_by_id = self._controller.get_instances_context_info( + active_instances_by_id.keys() + ) all_valid = None - for instance in self._controller.get_instances(): - if not instance["active"]: - continue - - if not instance.has_valid_context: + for instance_id, instance in active_instances_by_id.items(): + context_info = context_info_by_id[instance_id] + if not context_info.is_valid: all_valid = False break From 930e10f63dca9ced383327e2a31c1fcbd641ca3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:30:09 +0200 Subject: [PATCH 2/8] insntances with context promise have disabled inputs --- .../tools/publisher/widgets/widgets.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 4ad527126b..2427195812 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1182,6 +1182,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget): invalid_tasks = False folder_paths = [] for instance in self._current_instances: + # Ignore instances that have promised context + if instance.has_promised_context: + continue + new_variant_value = instance.get("variant") new_folder_path = instance.get("folderPath") new_task_name = instance.get("task") @@ -1303,7 +1307,13 @@ class GlobalAttrsWidget(QtWidgets.QWidget): editable = False folder_task_combinations = [] + context_editable = None for instance in instances: + if not instance.has_promised_context: + context_editable = True + elif context_editable is None: + context_editable = False + # NOTE I'm not sure how this can even happen? if instance.creator_identifier is None: editable = False @@ -1316,6 +1326,11 @@ class GlobalAttrsWidget(QtWidgets.QWidget): folder_task_combinations.append((folder_path, task_name)) product_names.add(instance.get("productName") or self.unknown_value) + if not editable: + context_editable = False + elif context_editable is None: + context_editable = True + self.variant_input.set_value(variants) # Set context of folder widget @@ -1326,8 +1341,21 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self.product_value_widget.set_value(product_names) self.variant_input.setEnabled(editable) - self.folder_value_widget.setEnabled(editable) - self.task_value_widget.setEnabled(editable) + self.folder_value_widget.setEnabled(context_editable) + self.task_value_widget.setEnabled(context_editable) + + if not editable: + folder_tooltip = "Select instances to change folder path." + task_tooltip = "Select instances to change task name." + elif not context_editable: + folder_tooltip = "Folder path is defined by Create plugin." + task_tooltip = "Task is defined by Create plugin." + else: + folder_tooltip = "Change folder path of selected instances." + task_tooltip = "Change task of selected instances." + + self.folder_value_widget.setToolTip(folder_tooltip) + self.task_value_widget.setToolTip(task_tooltip) class CreatorAttrsWidget(QtWidgets.QWidget): From f38fad901d164475500f7e0a5f5ab65a439655fe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Sep 2024 22:02:05 +0200 Subject: [PATCH 3/8] Allow `.tif` and `.tiff` extensions for review --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 4390b00754..06b451bfbe 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -95,7 +95,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ] # Supported extensions - image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga"] + image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"] video_exts = ["mov", "mp4"] supported_exts = image_exts + video_exts From e0ab613d965c79ba3e5944f3fe9f94541aca26ec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Sep 2024 19:55:36 +0200 Subject: [PATCH 4/8] Fix typos, ported from https://github.com/ynput/OpenPype/pull/6346 --- client/ayon_core/lib/attribute_definitions.py | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 7e022f6dba..fd0de2e41b 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -60,7 +60,7 @@ def get_default_values(attribute_definitions): for which default values should be collected. Returns: - Dict[str, Any]: Default values for passet attribute definitions. + Dict[str, Any]: Default values for passed attribute definitions. """ output = {} @@ -75,13 +75,13 @@ def get_default_values(attribute_definitions): class AbstractAttrDefMeta(ABCMeta): - """Metaclass to validate existence of 'key' attribute. + """Metaclass to validate the existence of 'key' attribute. - Each object of `AbstractAttrDef` mus have defined 'key' attribute. + Each object of `AbstractAttrDef` must have defined 'key' attribute. """ - def __call__(self, *args, **kwargs): - obj = super(AbstractAttrDefMeta, self).__call__(*args, **kwargs) + def __call__(cls, *args, **kwargs): + obj = super(AbstractAttrDefMeta, cls).__call__(*args, **kwargs) init_class = getattr(obj, "__init__class__", None) if init_class is not AbstractAttrDef: raise TypeError("{} super was not called in __init__.".format( @@ -162,7 +162,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def __ne__(self, other): return not self.__eq__(other) - @abstractproperty + @property + @abstractmethod def type(self): """Attribute definition type also used as identifier of class. @@ -215,10 +216,11 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): # ----------------------------------------- -# UI attribute definitoins won't hold value +# UI attribute definitions won't hold value # ----------------------------------------- class UIDef(AbstractAttrDef): + type = "ui-def" is_value_def = False def __init__(self, key=None, default=None, *args, **kwargs): @@ -245,7 +247,7 @@ class UILabelDef(UIDef): # --------------------------------------- -# Attribute defintioins should hold value +# Attribute definitions should hold value # --------------------------------------- class UnknownDef(AbstractAttrDef): @@ -311,7 +313,7 @@ class NumberDef(AbstractAttrDef): ): minimum = 0 if minimum is None else minimum maximum = 999999 if maximum is None else maximum - # Swap min/max when are passed in opposited order + # Swap min/max when are passed in opposite order if minimum > maximum: maximum, minimum = minimum, maximum @@ -364,10 +366,10 @@ class NumberDef(AbstractAttrDef): class TextDef(AbstractAttrDef): """Text definition. - Text can have multiline option so endline characters are allowed regex + Text can have multiline option so end-line characters are allowed regex validation can be applied placeholder for UI purposes and default value. - Regex validation is not part of attribute implemntentation. + Regex validation is not part of attribute implementation. Args: multiline(bool): Text has single or multiline support. @@ -949,7 +951,8 @@ def deserialize_attr_def(attr_def_data): """Deserialize attribute definition from data. Args: - attr_def (Dict[str, Any]): Attribute definition data to deserialize. + attr_def_data (Dict[str, Any]): Attribute definition data to + deserialize. """ attr_type = attr_def_data.pop("type") From 440d2271d46d3eeb49a763f459266b3740e18069 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Sep 2024 20:33:45 +0200 Subject: [PATCH 5/8] Fix variable redeclaration --- client/ayon_core/scripts/slates/slate_base/items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/scripts/slates/slate_base/items.py b/client/ayon_core/scripts/slates/slate_base/items.py index 6d19fc6a0c..ec3358ed5e 100644 --- a/client/ayon_core/scripts/slates/slate_base/items.py +++ b/client/ayon_core/scripts/slates/slate_base/items.py @@ -486,11 +486,11 @@ class TableField(BaseItem): line = self.ellide_text break - for idx, char in enumerate(_word): + for char_index, char in enumerate(_word): _line = line + char + self.ellide_text _line_width = font.getsize(_line)[0] if _line_width > max_width: - if idx == 0: + if char_index == 0: line = _line break line = line + char From 23ac8b9ca7117262db82e001b2eff636f89903d2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Sep 2024 15:49:42 +0200 Subject: [PATCH 6/8] Update client/ayon_core/lib/attribute_definitions.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/attribute_definitions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index fd0de2e41b..bda5bc2c47 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -220,7 +220,6 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): # ----------------------------------------- class UIDef(AbstractAttrDef): - type = "ui-def" is_value_def = False def __init__(self, key=None, default=None, *args, **kwargs): From f4c63805853014f5c31ea53d942e632fc3a753a4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Sep 2024 15:51:22 +0200 Subject: [PATCH 7/8] Remove unused import --- 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 fd0de2e41b..0a00e0e3ac 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -4,7 +4,7 @@ import collections import uuid import json import copy -from abc import ABCMeta, abstractmethod, abstractproperty +from abc import ABCMeta, abstractmethod import clique From ed2a3102039d84be7d830a6ca20a910daac3c514 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Sep 2024 15:51:57 +0200 Subject: [PATCH 8/8] Grammar --- 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 0a00e0e3ac..c2563333be 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -16,7 +16,7 @@ _attr_defs_by_type = {} def register_attr_def_class(cls): """Register attribute definition. - Currently are registered definitions used to deserialize data to objects. + Currently registered definitions are used to deserialize data to objects. Attrs: cls (AbstractAttrDef): Non-abstract class to be registered with unique