From 4ae8f277b99298018b999c1539e051fd82eb4de7 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 29 May 2024 22:29:13 +0300 Subject: [PATCH 01/60] add 'pyblish_debug_stepper' tool --- .../pyblish_debug_stepper.py | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py new file mode 100644 index 0000000000..e6cf3c69d6 --- /dev/null +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -0,0 +1,269 @@ +""" +Brought from https://gist.github.com/BigRoy/1972822065e38f8fae7521078e44eca2 +Code Credits: [BigRoy](https://github.com/BigRoy) + +Requirement: + This tool requires some modification in ayon-core. + Add the following two lines in sa similar fashion to this commit + https://github.com/ynput/OpenPype/commit/6a0ce21aa1f8cb17452fe066aa15134d22fda440 + i.e. Add them just after + https://github.com/ynput/ayon-core/blob/8366d2e8b4003a252b8da822f7e38c6db08292b4/client/ayon_core/tools/publisher/control.py#L2483-L2487 + + ``` + result["context"] = self._publish_context + pyblish.api.emit("pluginProcessedCustom", result=result) + ``` + + This modification should be temporary till the following PR get merged and released. + https://github.com/pyblish/pyblish-base/pull/401 + +How it works: + It registers a callback function `on_plugin_processed` + when event `pluginProcessedCustom` is emitted. + The logic of this function is roughly: + 1. Pauses the publishing. + 2. Collects some info about the plugin. + 3. Shows that info to the tool's window. + 4. Continues publishing on clicking `step` button. + +How to use it: + 1. Launch the tool from AYON experimental tools window. + 2. Launch the publisher tool and click validate. + 3. Click Step to run plugins one by one. + +Note: + It won't work when triggering validation from code as our custom event lives inside ayon-core. + But, It should work when the mentioned PR above (#401) get merged and released. + +""" + +import copy +import json +from qtpy import QtWidgets, QtCore, QtGui + +import pyblish.api +from ayon_core import style + +TAB = 4* " " +HEADER_SIZE = "15px" + +KEY_COLOR = QtGui.QColor("#ffffff") +NEW_KEY_COLOR = QtGui.QColor("#00ff00") +VALUE_TYPE_COLOR = QtGui.QColor("#ffbbbb") +NEW_VALUE_TYPE_COLOR = QtGui.QColor("#ff4444") +VALUE_COLOR = QtGui.QColor("#777799") +NEW_VALUE_COLOR = QtGui.QColor("#DDDDCC") +CHANGED_VALUE_COLOR = QtGui.QColor("#CCFFCC") + +MAX_VALUE_STR_LEN = 100 + + +def failsafe_deepcopy(data): + """Allow skipping the deepcopy for unsupported types""" + try: + return copy.deepcopy(data) + except TypeError: + if isinstance(data, dict): + return { + key: failsafe_deepcopy(value) + for key, value in data.items() + } + elif isinstance(data, list): + return data.copy() + return data + + +class DictChangesModel(QtGui.QStandardItemModel): + # TODO: Replace this with a QAbstractItemModel + def __init__(self, *args, **kwargs): + super(DictChangesModel, self).__init__(*args, **kwargs) + self._data = {} + + columns = ["Key", "Type", "Value"] + self.setColumnCount(len(columns)) + for i, label in enumerate(columns): + self.setHeaderData(i, QtCore.Qt.Horizontal, label) + + def _update_recursive(self, data, parent, previous_data): + for key, value in data.items(): + + # Find existing item or add new row + parent_index = parent.index() + for row in range(self.rowCount(parent_index)): + # Update existing item if it exists + index = self.index(row, 0, parent_index) + if index.data() == key: + item = self.itemFromIndex(index) + type_item = self.itemFromIndex(self.index(row, 1, parent_index)) + value_item = self.itemFromIndex(self.index(row, 2, parent_index)) + break + else: + item = QtGui.QStandardItem(key) + type_item = QtGui.QStandardItem() + value_item = QtGui.QStandardItem() + parent.appendRow([item, type_item, value_item]) + + # Key + key_color = NEW_KEY_COLOR if key not in previous_data else KEY_COLOR + item.setData(key_color, QtCore.Qt.ForegroundRole) + + # Type + type_str = type(value).__name__ + type_color = VALUE_TYPE_COLOR + if key in previous_data and type(previous_data[key]).__name__ != type_str: + type_color = NEW_VALUE_TYPE_COLOR + + type_item.setText(type_str) + type_item.setData(type_color, QtCore.Qt.ForegroundRole) + + # Value + value_changed = False + if key not in previous_data or previous_data[key] != value: + value_changed = True + value_color = NEW_VALUE_COLOR if value_changed else VALUE_COLOR + + value_item.setData(value_color, QtCore.Qt.ForegroundRole) + if value_changed: + value_str = str(value) + if len(value_str) > MAX_VALUE_STR_LEN: + value_str = value_str[:MAX_VALUE_STR_LEN] + "..." + value_item.setText(value_str) + # Preferably this is deferred to only when the data gets requested + # since this formatting can be slow for very large data sets like + # project settings and system settings + # This will also be MUCH MUCH faster if we don't clear the items on each update + # but only updated/add/remove changed items so that this also runs much less often + value_item.setData(json.dumps(value, default=str, indent=4), QtCore.Qt.ToolTipRole) + + + if isinstance(value, dict): + previous_value = previous_data.get(key, {}) + if previous_data.get(key) != value: + # Update children if the value is not the same as before + self._update_recursive(value, parent=item, previous_data=previous_value) + else: + # TODO: Ensure all children are updated to be not marked as 'changed' + # in the most optimal way possible + self._update_recursive(value, parent=item, previous_data=previous_value) + + self._data = data + + def update(self, data): + parent = self.invisibleRootItem() + + data = failsafe_deepcopy(data) + previous_data = self._data + self._update_recursive(data, parent, previous_data) + self._data = data # store previous data for next update + + +class DebugUI(QtWidgets.QDialog): + + def __init__(self, parent=None): + super(DebugUI, self).__init__(parent=parent) + self.setStyleSheet(style.load_stylesheet()) + + self._set_window_title() + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowMinimizeButtonHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + + layout = QtWidgets.QVBoxLayout(self) + text_edit = QtWidgets.QTextEdit() + text_edit.setFixedHeight(65) + font = QtGui.QFont("NONEXISTENTFONT") + font.setStyleHint(font.TypeWriter) + text_edit.setFont(font) + text_edit.setLineWrapMode(text_edit.NoWrap) + + step = QtWidgets.QPushButton("Step") + step.setEnabled(False) + + model = DictChangesModel() + proxy = QtCore.QSortFilterProxyModel() + proxy.setSourceModel(model) + view = QtWidgets.QTreeView() + view.setModel(proxy) + view.setSortingEnabled(True) + + layout.addWidget(text_edit) + layout.addWidget(view) + layout.addWidget(step) + + step.clicked.connect(self.on_step) + + self._pause = False + self.model = model + self.proxy = proxy + self.view = view + self.text = text_edit + self.step = step + self.resize(700, 500) + + self._previous_data = {} + + + + def _set_window_title(self, plugin=None): + title = "Pyblish Debug Stepper" + if plugin is not None: + plugin_label = plugin.label or plugin.__name__ + title += f" | {plugin_label}" + self.setWindowTitle(title) + + def pause(self, state): + self._pause = state + self.step.setEnabled(state) + + def on_step(self): + self.pause(False) + + def showEvent(self, event): + print("Registering callback..") + pyblish.api.register_callback("pluginProcessedCustom", # "pluginProcessed" + self.on_plugin_processed) + + def hideEvent(self, event): + self.pause(False) + print("Deregistering callback..") + pyblish.api.deregister_callback("pluginProcessedCustom", # "pluginProcessed" + self.on_plugin_processed) + + def on_plugin_processed(self, result): + self.pause(True) + + self._set_window_title(plugin=result["plugin"]) + + print(10*"<" ,result["plugin"].__name__, 10*">") + + plugin_order = result["plugin"].order + plugin_name = result["plugin"].__name__ + duration = result['duration'] + plugin_instance = result["instance"] + context = result["context"] + + msg = "" + msg += f"Order: {plugin_order}
" + msg += f"Plugin: {plugin_name}" + if plugin_instance is not None: + msg += f" -> instance: {plugin_instance}" + msg += "
" + msg += f"Duration: {duration} ms
" + self.text.setHtml(msg) + + data = { + "context": context.data + } + for instance in context: + data[instance.name] = instance.data + self.model.update(data) + + app = QtWidgets.QApplication.instance() + while self._pause: + # Allow user interaction with the UI + app.processEvents() From 3de28f870d54415fc0d14e454f770772cf11d554 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 24 Jun 2024 23:12:23 +0300 Subject: [PATCH 02/60] Add DebugExprimentalTool to ExperimentalTools --- .../tools/experimental_tools/tools_def.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/client/ayon_core/tools/experimental_tools/tools_def.py b/client/ayon_core/tools/experimental_tools/tools_def.py index 7def3551de..e3acfa72ab 100644 --- a/client/ayon_core/tools/experimental_tools/tools_def.py +++ b/client/ayon_core/tools/experimental_tools/tools_def.py @@ -1,4 +1,5 @@ import os +from .pyblish_debug_stepper import DebugUI # Constant key under which local settings are stored LOCAL_EXPERIMENTAL_KEY = "experimental_tools" @@ -95,6 +96,12 @@ class ExperimentalTools: "hiero", "resolve", ] + ), + ExperimentalHostTool( + "DebugExprimentalTool", + "Pyblish Debug Stepper", + "Debug Pyblish plugins step by step.", + self._show_pyblish_debugger, ) ] @@ -162,6 +169,13 @@ class ExperimentalTools: local_settings.get(LOCAL_EXPERIMENTAL_KEY) ) or {} + # Enable the following tools by default. + # Because they will always be disabled due + # to the fact their settings don't exist. + experimental_settings.update({ + "DebugExprimentalTool": True, + }) + for identifier, eperimental_tool in self.tools_by_identifier.items(): enabled = experimental_settings.get(identifier, False) eperimental_tool.set_enabled(enabled) @@ -175,3 +189,7 @@ class ExperimentalTools: ) self._publisher_tool.show() + + def _show_pyblish_debugger(self): + window = DebugUI(parent=self._parent_widget) + window.show() From 36caeba8dbd5ed3da3f736bebed52330f90b12a9 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 24 Jun 2024 23:26:34 +0300 Subject: [PATCH 03/60] Update pyblish debug stepper docstring --- .../pyblish_debug_stepper.py | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py index e6cf3c69d6..51b7ba2c06 100644 --- a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -3,37 +3,25 @@ Brought from https://gist.github.com/BigRoy/1972822065e38f8fae7521078e44eca2 Code Credits: [BigRoy](https://github.com/BigRoy) Requirement: - This tool requires some modification in ayon-core. - Add the following two lines in sa similar fashion to this commit - https://github.com/ynput/OpenPype/commit/6a0ce21aa1f8cb17452fe066aa15134d22fda440 - i.e. Add them just after - https://github.com/ynput/ayon-core/blob/8366d2e8b4003a252b8da822f7e38c6db08292b4/client/ayon_core/tools/publisher/control.py#L2483-L2487 - - ``` - result["context"] = self._publish_context - pyblish.api.emit("pluginProcessedCustom", result=result) - ``` - - This modification should be temporary till the following PR get merged and released. - https://github.com/pyblish/pyblish-base/pull/401 + It requires pyblish version >= 1.8.12 How it works: - It registers a callback function `on_plugin_processed` - when event `pluginProcessedCustom` is emitted. - The logic of this function is roughly: - 1. Pauses the publishing. - 2. Collects some info about the plugin. - 3. Shows that info to the tool's window. - 4. Continues publishing on clicking `step` button. + This tool makes use of pyblish event `pluginProcessed` to: + 1. Pause the publishing. + 2. Collect some info about the plugin. + 3. Show that info to the tool's window. + 4. Continue publishing on clicking `step` button. How to use it: 1. Launch the tool from AYON experimental tools window. 2. Launch the publisher tool and click validate. 3. Click Step to run plugins one by one. -Note: - It won't work when triggering validation from code as our custom event lives inside ayon-core. - But, It should work when the mentioned PR above (#401) get merged and released. +Note : + Pyblish debugger also works when triggering the validation or + publishing from code. + Here's an example about validating from code: + https://github.com/MustafaJafar/ayon-recipes/blob/main/validate_from_code.py """ @@ -225,13 +213,13 @@ class DebugUI(QtWidgets.QDialog): def showEvent(self, event): print("Registering callback..") - pyblish.api.register_callback("pluginProcessedCustom", # "pluginProcessed" + pyblish.api.register_callback("pluginProcessed", self.on_plugin_processed) def hideEvent(self, event): self.pause(False) print("Deregistering callback..") - pyblish.api.deregister_callback("pluginProcessedCustom", # "pluginProcessed" + pyblish.api.deregister_callback("pluginProcessed", self.on_plugin_processed) def on_plugin_processed(self, result): From f5006273ae69f6cb6404522488ae3141ed376200 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 5 Jul 2024 19:33:23 +0300 Subject: [PATCH 04/60] fix typo --- client/ayon_core/tools/experimental_tools/tools_def.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/experimental_tools/tools_def.py b/client/ayon_core/tools/experimental_tools/tools_def.py index e3acfa72ab..30e5211b41 100644 --- a/client/ayon_core/tools/experimental_tools/tools_def.py +++ b/client/ayon_core/tools/experimental_tools/tools_def.py @@ -98,7 +98,7 @@ class ExperimentalTools: ] ), ExperimentalHostTool( - "DebugExprimentalTool", + "pyblish_debug_stepper", "Pyblish Debug Stepper", "Debug Pyblish plugins step by step.", self._show_pyblish_debugger, @@ -173,12 +173,12 @@ class ExperimentalTools: # Because they will always be disabled due # to the fact their settings don't exist. experimental_settings.update({ - "DebugExprimentalTool": True, + "pyblish_debug_stepper": True, }) - for identifier, eperimental_tool in self.tools_by_identifier.items(): + for identifier, experimental_tool in self.tools_by_identifier.items(): enabled = experimental_settings.get(identifier, False) - eperimental_tool.set_enabled(enabled) + experimental_tool.set_enabled(enabled) def _show_publisher(self): if self._publisher_tool is None: From d01cde7051527c85738b13f1c3769d821e8b0f3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:01:42 +0200 Subject: [PATCH 05/60] reverse disabled and hidden to positive names --- client/ayon_core/lib/attribute_definitions.py | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 894b012d59..a882bee0d9 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -90,6 +90,26 @@ class AbstractAttrDefMeta(ABCMeta): return obj +def _convert_reversed_attr( + main_value, depr_value, main_label, depr_label, default +): + if main_value is not None and depr_value is not None: + if main_value == depr_value: + print( + f"God invalid '{main_label}' and '{depr_label}' arguments." + f" Using '{main_label}' value." + ) + elif depr_value is not None: + print( + f"Using deprecated argument '{depr_label}'" + f" please use '{main_label}' instead." + ) + main_value = not depr_value + elif main_value is None: + main_value = default + return main_value + + class AbstractAttrDef(metaclass=AbstractAttrDefMeta): """Abstraction of attribute definition. @@ -106,12 +126,14 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Args: key (str): Under which key will be attribute value stored. default (Any): Default value of an attribute. - label (str): Attribute label. - tooltip (str): Attribute tooltip. - is_label_horizontal (bool): UI specific argument. Specify if label is - next to value input or ahead. - hidden (bool): Will be item hidden (for UI purposes). - disabled (bool): Item will be visible but disabled (for UI purposes). + label (Optional[str]): Attribute label. + tooltip (Optional[str]): Attribute tooltip. + is_label_horizontal (Optional[bool]): UI specific argument. Specify + if label is next to value input or ahead. + visible (Optional[bool]): Item is shown to user (for UI purposes). + 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 = [] @@ -125,22 +147,28 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): label=None, tooltip=None, is_label_horizontal=None, - hidden=False, - disabled=False + visible=None, + enabled=None, + hidden=None, + disabled=None, ): if is_label_horizontal is None: is_label_horizontal = True - if hidden is None: - hidden = False + enabled = _convert_reversed_attr( + enabled, disabled, "enabled", "disabled", True + ) + visible = _convert_reversed_attr( + visible, hidden, "visible", "hidden", True + ) self.key = key self.label = label self.tooltip = tooltip self.default = default self.is_label_horizontal = is_label_horizontal - self.hidden = hidden - self.disabled = disabled + self.visible = visible + self.enabled = enabled self._id = uuid.uuid4().hex self.__init__class__ = AbstractAttrDef @@ -149,14 +177,30 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def id(self): return self._id + @property + def hidden(self): + return not self.visible + + @hidden.setter + def hidden(self, value): + self.visible = not value + + @property + def disabled(self): + return not self.enabled + + @disabled.setter + def disabled(self, value): + self.enabled = not value + def __eq__(self, other): if not isinstance(other, self.__class__): return False return ( self.key == other.key - and self.hidden == other.hidden and self.default == other.default - and self.disabled == other.disabled + and self.visible == other.visible + and self.enabled == other.enabled ) def __ne__(self, other): @@ -198,8 +242,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): "tooltip": self.tooltip, "default": self.default, "is_label_horizontal": self.is_label_horizontal, - "hidden": self.hidden, - "disabled": self.disabled + "visible": self.visible, + "enabled": self.enabled } for attr in self.type_attributes: data[attr] = getattr(self, attr) @@ -279,8 +323,8 @@ class HiddenDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): kwargs["default"] = default - kwargs["hidden"] = True - super(HiddenDef, self).__init__(key, **kwargs) + kwargs["visible"] = False + super().__init__(key, **kwargs) def convert_value(self, value): return value From f174941e1d11fc2d04b85d1de69db8441ff9d6c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:01:54 +0200 Subject: [PATCH 06/60] use new attribute names --- client/ayon_core/tools/attribute_defs/widgets.py | 6 +++--- client/ayon_core/tools/publisher/widgets/widgets.py | 4 ++-- 2 files 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 5ead3f46a6..026aea00ad 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -28,10 +28,10 @@ from .files_widget import FilesWidget def create_widget_for_attr_def(attr_def, parent=None): widget = _create_widget_for_attr_def(attr_def, parent) - if attr_def.hidden: + if not attr_def.visible: widget.setVisible(False) - if attr_def.disabled: + if not attr_def.enabled: widget.setEnabled(False) return widget @@ -135,7 +135,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): widget = create_widget_for_attr_def(attr_def, self) self._widgets.append(widget) - if attr_def.hidden: + if not attr_def.visible: continue expand_cols = 2 diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 83a2d9e6c1..b0f32dfcfc 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1446,7 +1446,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_instances[attr_def.id] = attr_instances self._attr_def_id_to_attr_def[attr_def.id] = attr_def - if attr_def.hidden: + if not attr_def.visible: continue expand_cols = 2 @@ -1585,7 +1585,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget = create_widget_for_attr_def( attr_def, content_widget ) - hidden_widget = attr_def.hidden + hidden_widget = not attr_def.visible # Hide unknown values of publish plugins # - The keys in most of cases does not represent what would # label represent From 37697bc6ce62f320da88b3bea1f933e424714acd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:06:40 +0200 Subject: [PATCH 07/60] change print to warning --- client/ayon_core/lib/attribute_definitions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index a882bee0d9..cffa424798 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -4,6 +4,7 @@ import collections import uuid import json import copy +import warnings from abc import ABCMeta, abstractmethod import clique @@ -100,9 +101,13 @@ def _convert_reversed_attr( f" Using '{main_label}' value." ) elif depr_value is not None: - print( - f"Using deprecated argument '{depr_label}'" - f" please use '{main_label}' instead." + warnings.warn( + ( + "DEPRECATION WARNING: Using deprecated argument" + f" '{depr_label}' please use '{main_label}' instead." + ), + DeprecationWarning, + stacklevel=4, ) main_value = not depr_value elif main_value is None: From b6392a3e4201ca3f2dfc75a7a25bb1b3834783be Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:20:15 +0200 Subject: [PATCH 08/60] fix comment Co-authored-by: Roy Nieterau --- 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 cffa424798..639778b16d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -97,7 +97,7 @@ def _convert_reversed_attr( if main_value is not None and depr_value is not None: if main_value == depr_value: print( - f"God invalid '{main_label}' and '{depr_label}' arguments." + f"Got invalid '{main_label}' and '{depr_label}' arguments." f" Using '{main_label}' value." ) elif depr_value is not None: From b905dfe4bbc68e32cd76bb9fdea226c9c68e2a29 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 24 Sep 2024 02:41:30 +0200 Subject: [PATCH 09/60] Support PySide6 --- .../tools/experimental_tools/pyblish_debug_stepper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py index 51b7ba2c06..1db0a3d9f4 100644 --- a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -165,9 +165,9 @@ class DebugUI(QtWidgets.QDialog): text_edit = QtWidgets.QTextEdit() text_edit.setFixedHeight(65) font = QtGui.QFont("NONEXISTENTFONT") - font.setStyleHint(font.TypeWriter) + font.setStyleHint(QtGui.QFont.TypeWriter) text_edit.setFont(font) - text_edit.setLineWrapMode(text_edit.NoWrap) + text_edit.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) step = QtWidgets.QPushButton("Step") step.setEnabled(False) From c9ea3dddd06dcdb15714f977095f49b513ea3f66 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 24 Sep 2024 02:44:36 +0200 Subject: [PATCH 10/60] Cosmetics --- .../pyblish_debug_stepper.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py index 1db0a3d9f4..0f1120b8f6 100644 --- a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -82,8 +82,8 @@ class DictChangesModel(QtGui.QStandardItemModel): index = self.index(row, 0, parent_index) if index.data() == key: item = self.itemFromIndex(index) - type_item = self.itemFromIndex(self.index(row, 1, parent_index)) - value_item = self.itemFromIndex(self.index(row, 2, parent_index)) + type_item = self.itemFromIndex(self.index(row, 1, parent_index)) # noqa + value_item = self.itemFromIndex(self.index(row, 2, parent_index)) # noqa break else: item = QtGui.QStandardItem(key) @@ -92,13 +92,16 @@ class DictChangesModel(QtGui.QStandardItemModel): parent.appendRow([item, type_item, value_item]) # Key - key_color = NEW_KEY_COLOR if key not in previous_data else KEY_COLOR + key_color = NEW_KEY_COLOR if key not in previous_data else KEY_COLOR # noqa item.setData(key_color, QtCore.Qt.ForegroundRole) # Type type_str = type(value).__name__ type_color = VALUE_TYPE_COLOR - if key in previous_data and type(previous_data[key]).__name__ != type_str: + if ( + key in previous_data + and type(previous_data[key]).__name__ != type_str + ): type_color = NEW_VALUE_TYPE_COLOR type_item.setText(type_str) @@ -116,23 +119,31 @@ class DictChangesModel(QtGui.QStandardItemModel): if len(value_str) > MAX_VALUE_STR_LEN: value_str = value_str[:MAX_VALUE_STR_LEN] + "..." value_item.setText(value_str) - # Preferably this is deferred to only when the data gets requested - # since this formatting can be slow for very large data sets like - # project settings and system settings - # This will also be MUCH MUCH faster if we don't clear the items on each update - # but only updated/add/remove changed items so that this also runs much less often - value_item.setData(json.dumps(value, default=str, indent=4), QtCore.Qt.ToolTipRole) + # Preferably this is deferred to only when the data gets + # requested since this formatting can be slow for very large + # data sets like project settings and system settings + # This will also be MUCH faster if we don't clear the + # items on each update but only updated/add/remove changed + # items so that this also runs much less often + value_item.setData( + json.dumps(value, default=str, indent=4), + QtCore.Qt.ToolTipRole + ) if isinstance(value, dict): previous_value = previous_data.get(key, {}) if previous_data.get(key) != value: # Update children if the value is not the same as before - self._update_recursive(value, parent=item, previous_data=previous_value) + self._update_recursive(value, + parent=item, + previous_data=previous_value) else: - # TODO: Ensure all children are updated to be not marked as 'changed' - # in the most optimal way possible - self._update_recursive(value, parent=item, previous_data=previous_value) + # TODO: Ensure all children are updated to be not marked + # as 'changed' in the most optimal way possible + self._update_recursive(value, + parent=item, + previous_data=previous_value) self._data = data @@ -195,8 +206,6 @@ class DebugUI(QtWidgets.QDialog): self._previous_data = {} - - def _set_window_title(self, plugin=None): title = "Pyblish Debug Stepper" if plugin is not None: @@ -227,7 +236,7 @@ class DebugUI(QtWidgets.QDialog): self._set_window_title(plugin=result["plugin"]) - print(10*"<" ,result["plugin"].__name__, 10*">") + print(10*"<", result["plugin"].__name__, 10*">") plugin_order = result["plugin"].order plugin_name = result["plugin"].__name__ From b946ed64f33f3c1e19c351b44f8c1d55cf395738 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 24 Sep 2024 02:51:49 +0200 Subject: [PATCH 11/60] Add simple filter field to quickly filter to certain keys only --- .../tools/experimental_tools/pyblish_debug_stepper.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py index 0f1120b8f6..33de4bf036 100644 --- a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -185,12 +185,18 @@ class DebugUI(QtWidgets.QDialog): model = DictChangesModel() proxy = QtCore.QSortFilterProxyModel() + proxy.setRecursiveFilteringEnabled(True) proxy.setSourceModel(model) view = QtWidgets.QTreeView() view.setModel(proxy) view.setSortingEnabled(True) + filter_field = QtWidgets.QLineEdit() + filter_field.setPlaceholderText("Filter keys...") + filter_field.textChanged.connect(proxy.setFilterFixedString) + layout.addWidget(text_edit) + layout.addWidget(filter_field) layout.addWidget(view) layout.addWidget(step) @@ -198,6 +204,7 @@ class DebugUI(QtWidgets.QDialog): self._pause = False self.model = model + self.filter = filter_field self.proxy = proxy self.view = view self.text = text_edit From 1a64490256063dc33fd9e9990329e5182b339838 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 00:26:39 +0200 Subject: [PATCH 12/60] Allow representation switch on update if representation does not exist (e.g. `ma` -> `mb` representation) --- client/ayon_core/pipeline/load/plugins.py | 20 +++++++++ client/ayon_core/pipeline/load/utils.py | 49 ++++++++++++++++------- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 2475800cbb..28a7e775d6 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -242,6 +242,26 @@ class LoaderPlugin(list): if hasattr(self, "_fname"): return self._fname + def update_allowed_representation_switches(self): + """Return a mapping from source representation names to ordered + destination representation names to which switching is allowed if + the source representation name does not exist for the new version. + + For example, to allow an automated switch on update from representation + `ma` to `mb` or `abc` if the new version does not have a `ma` + representation you can return: + {"ma": ["mb", "abc"]} + + The order of the names in the returned values is important, because + if `ma` is missing and both of the replacement representations are + present than the first one will be chosen. + + Returns: + Dict[str, List[str]]: Mapping from representation names to allowed + alias representation names switching to is allowed on update. + """ + return {} + class ProductLoaderPlugin(LoaderPlugin): """Load product into host application diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 9ba407193e..bb74194ea1 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -505,21 +505,6 @@ def update_container(container, version=-1): project_name, product_entity["folderId"] ) - repre_name = current_representation["name"] - new_representation = ayon_api.get_representation_by_name( - project_name, repre_name, new_version["id"] - ) - if new_representation is None: - raise ValueError( - "Representation '{}' wasn't found on requested version".format( - repre_name - ) - ) - - path = get_representation_path(new_representation) - if not path or not os.path.exists(path): - raise ValueError("Path {} doesn't exist".format(path)) - # Run update on the Loader for this container Loader = _get_container_loader(container) if not Loader: @@ -527,6 +512,40 @@ def update_container(container, version=-1): "Can't update container because loader '{}' was not found." .format(container.get("loader")) ) + + repre_name = current_representation["name"] + new_representation = ayon_api.get_representation_by_name( + project_name, repre_name, new_version["id"] + ) + if new_representation is None: + # The representation name is not found in the new version. + # Allow updating to a 'matching' representation if the loader + # has defined compatible update conversions + mapping = Loader.update_allowed_representation_switches() + switch_repre_names = mapping.get(repre_name) + if switch_repre_names: + representations = ayon_api.get_representations( + project_name, + representation_names=switch_repre_names, + version_ids=[new_version["id"]]) + representations_by_name = { + repre["name"]: repre for repre in representations + } + for name in switch_repre_names: + if name in representations_by_name: + new_representation = representations_by_name[name] + break + + if new_representation is None: + raise ValueError( + "Representation '{}' wasn't found on requested version".format( + repre_name + ) + ) + + path = get_representation_path(new_representation) + if not path or not os.path.exists(path): + raise ValueError("Path {} doesn't exist".format(path)) project_entity = ayon_api.get_project(project_name) context = { "project": project_entity, From 1dbaa57e0926fabc6afa9b85a5ae3e7856dccb07 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 00:39:04 +0200 Subject: [PATCH 13/60] Fix call to method --- client/ayon_core/pipeline/load/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index bb74194ea1..b258d20a3d 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -521,7 +521,7 @@ def update_container(container, version=-1): # The representation name is not found in the new version. # Allow updating to a 'matching' representation if the loader # has defined compatible update conversions - mapping = Loader.update_allowed_representation_switches() + mapping = Loader().update_allowed_representation_switches() switch_repre_names = mapping.get(repre_name) if switch_repre_names: representations = ayon_api.get_representations( From 266140ad40f7a651244220533257eda57aa305ba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 19:23:37 +0200 Subject: [PATCH 14/60] Refactor function `update_allowed_representation_switches` -> `get_representation_name_aliases` --- client/ayon_core/pipeline/load/plugins.py | 26 +++++++++++------------ client/ayon_core/pipeline/load/utils.py | 9 ++++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 28a7e775d6..1fb906fd65 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -242,25 +242,25 @@ class LoaderPlugin(list): if hasattr(self, "_fname"): return self._fname - def update_allowed_representation_switches(self): - """Return a mapping from source representation names to ordered - destination representation names to which switching is allowed if - the source representation name does not exist for the new version. + @classmethod + def get_representation_name_aliases(cls, representation_name: str): + """Return representation names to which switching is allowed from + the input representation name, like an alias replacement of the input + `representation_name`. For example, to allow an automated switch on update from representation - `ma` to `mb` or `abc` if the new version does not have a `ma` - representation you can return: - {"ma": ["mb", "abc"]} + `ma` to `mb` or `abc`, then when `representation_name` is `ma` return: + ["mb", "abc"] - The order of the names in the returned values is important, because - if `ma` is missing and both of the replacement representations are - present than the first one will be chosen. + The order of the names in the returned representation names is + important, because the first one existing under the new version will + be chosen. Returns: - Dict[str, List[str]]: Mapping from representation names to allowed - alias representation names switching to is allowed on update. + List[str]: Representation names switching to is allowed on update + if the input representation name is not found on the new version. """ - return {} + return [] class ProductLoaderPlugin(LoaderPlugin): diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index b258d20a3d..ee2c1af07f 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -521,17 +521,16 @@ def update_container(container, version=-1): # The representation name is not found in the new version. # Allow updating to a 'matching' representation if the loader # has defined compatible update conversions - mapping = Loader().update_allowed_representation_switches() - switch_repre_names = mapping.get(repre_name) - if switch_repre_names: + repre_name_aliases = Loader.get_representation_name_aliases(repre_name) + if repre_name_aliases: representations = ayon_api.get_representations( project_name, - representation_names=switch_repre_names, + representation_names=repre_name_aliases, version_ids=[new_version["id"]]) representations_by_name = { repre["name"]: repre for repre in representations } - for name in switch_repre_names: + for name in repre_name_aliases: if name in representations_by_name: new_representation = representations_by_name[name] break From 84d6daf60c357735588f669e4c35a4d5f52febdc Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Oct 2024 08:36:34 -0400 Subject: [PATCH 15/60] Fix NTSC framerates floating issue comparison. --- client/ayon_core/pipeline/editorial.py | 24 +- .../resources/img_seq_23.976_metadata.json | 255 ++++++++++++++++++ .../test_media_range_with_retimes.py | 21 ++ 3 files changed, 293 insertions(+), 7 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_23.976_metadata.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index f382f91fec..af2a6ef88c 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -292,13 +292,23 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # Note that 24fps is slower than 25fps hence extended duration # to preserve media range - # Compute new source range based on available rate - conformed_src_in = source_range.start_time.rescaled_to(available_range_rate) - conformed_src_duration = source_range.duration.rescaled_to(available_range_rate) - conformed_source_range = otio.opentime.TimeRange( - start_time=conformed_src_in, - duration=conformed_src_duration - ) + # Compute new source range based on available rate. + # NSTC compatibility might introduce floating rates, when these are + # not exactly the same (23.976 vs 23.976024627685547) + # this will cause precision issue in computation. + # Round to 2 decimals for comparison. + rounded_av_rate = round(available_range_rate, 2) + rounded_src_rate = round(source_range.start_time.rate, 2) + if rounded_av_rate != rounded_src_rate: + conformed_src_in = source_range.start_time.rescaled_to(available_range_rate) + conformed_src_duration = source_range.duration.rescaled_to(available_range_rate) + conformed_source_range = otio.opentime.TimeRange( + start_time=conformed_src_in, + duration=conformed_src_duration + ) + + else: + conformed_source_range = source_range # modifiers time_scalar = 1. diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_23.976_metadata.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_23.976_metadata.json new file mode 100644 index 0000000000..af74ab4252 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_23.976_metadata.json @@ -0,0 +1,255 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "active": true, + "applieswhole": 1, + "asset": "sh020", + "audio": true, + "families": [ + "clip" + ], + "family": "plate", + "handleEnd": 8, + "handleStart": 0, + "heroTrack": true, + "hierarchy": "shots/sq001", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq001", + "shot": "sh020", + "track": "reference" + }, + "hiero_source_type": "TrackItem", + "id": "pyblish.avalon.instance", + "label": "openpypeData", + "note": "OpenPype data container", + "parents": [ + { + "entity_name": "shots", + "entity_type": "folder" + }, + { + "entity_name": "sq001", + "entity_type": "sequence" + } + ], + "publish": true, + "reviewTrack": null, + "sourceResolution": false, + "subset": "plateP01", + "variant": "Main", + "workfileFrameStart": 1001 + }, + "name": "sh020", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 51.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "active": true, + "applieswhole": 1, + "asset": "sh020", + "audio": true, + "families": [ + "clip" + ], + "family": "plate", + "handleEnd": 8, + "handleStart": 0, + "heroTrack": true, + "hierarchy": "shots/sq001", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "sq001", + "shot": "sh020", + "track": "reference" + }, + "hiero_source_type": "TrackItem", + "id": "pyblish.avalon.instance", + "label": "openpypeData", + "note": "OpenPype data container", + "parents": [ + { + "entity_name": "shots", + "entity_type": "folder" + }, + { + "entity_name": "sq001", + "entity_type": "sequence" + } + ], + "publish": true, + "reviewTrack": null, + "sourceResolution": false, + "subset": "plateP01", + "variant": "Main", + "workfileFrameStart": 1001 + }, + "name": "openpypeData", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + }, + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": 1, + "family": "task", + "hiero_source_type": "TrackItem", + "label": "comp", + "note": "Compositing", + "type": "Compositing" + }, + "name": "comp", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "59", + "foundry.source.filename": "MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.filesize": "", + "foundry.source.fragments": "59", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "MER_sq001_sh020_P01.%04d.exr 997-1055", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "997", + "foundry.source.timecode": "172800", + "foundry.source.umid": "1bf7437a-b446-440c-07c5-7cae7acf4f5e", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "59", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAMAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "8", + "media.exr.compressionName": "DWAA", + "media.exr.dataWindow": "0,0,1919,1079", + "media.exr.displayWindow": "0,0,1919,1079", + "media.exr.dwaCompressionLevel": "90", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2022-04-21 11:56:03", + "media.input.filename": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.0997.exr", + "media.input.filereader": "exr", + "media.input.filesize": "1235182", + "media.input.frame": "1", + "media.input.frame_rate": "23.976", + "media.input.height": "1080", + "media.input.mtime": "2022-03-06 10:14:41", + "media.input.timecode": "02:00:00:00", + "media.input.width": "1920", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "ffffffffffffffff", + "media.nuke.version": "12.2v3", + "openpype.source.colourtransform": "ACES - ACES2065-1", + "openpype.source.height": 1080, + "openpype.source.pixelAspect": 1.0, + "openpype.source.width": 1920, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 59.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 997.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01\\", + "name_prefix": "MER_sq001_sh020_P01.", + "name_suffix": ".exr", + "start_frame": 997, + "frame_step": 1, + "rate": 23.976, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index e5f0d335b5..7f9256c6d8 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -166,3 +166,24 @@ def test_img_sequence_relative_source_range(): "legacy_img_sequence.json", expected_data ) + +def test_img_sequence_conform_to_23_976fps(): + """ + Img sequence clip + available files = 997-1047 23.976fps + source_range = 997-1055 23.976024627685547fps + """ + expected_data = { + 'mediaIn': 997, + 'mediaOut': 1047, + 'handleStart': 0, + 'handleEnd': 8, + 'speed': 1.0 + } + + _check_expected_retimed_values( + "img_seq_23.976_metadata.json", + expected_data, + handle_start=0, + handle_end=8, + ) From a3c9106f35dd3c5876cb216ff4a9ad4f1ba9cac4 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Oct 2024 09:07:26 -0400 Subject: [PATCH 16/60] Fix typo --- client/ayon_core/pipeline/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index af2a6ef88c..94b101d3d3 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -293,7 +293,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # to preserve media range # Compute new source range based on available rate. - # NSTC compatibility might introduce floating rates, when these are + # NTSC compatibility might introduce floating rates, when these are # not exactly the same (23.976 vs 23.976024627685547) # this will cause precision issue in computation. # Round to 2 decimals for comparison. From 412b4b8d3a558449d2d29ef3353cef22dcddfc71 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 3 Oct 2024 10:18:12 -0400 Subject: [PATCH 17/60] Address feedback from PR. --- client/ayon_core/pipeline/editorial.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 94b101d3d3..a49a981d2a 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -293,10 +293,13 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # to preserve media range # Compute new source range based on available rate. + + # Backward-compatibility for Hiero OTIO exporter. # NTSC compatibility might introduce floating rates, when these are # not exactly the same (23.976 vs 23.976024627685547) # this will cause precision issue in computation. - # Round to 2 decimals for comparison. + # Currently round to 2 decimals for comparison, + # but this should always rescale after that. rounded_av_rate = round(available_range_rate, 2) rounded_src_rate = round(source_range.start_time.rate, 2) if rounded_av_rate != rounded_src_rate: From 162a47db60d61a80774b86b00daddcae7d34298c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 3 Oct 2024 14:27:19 +0000 Subject: [PATCH 18/60] [Automated] Add generated package files from 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 75116c703e..e6d6c6f373 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.0+dev" +__version__ = "1.0.1" diff --git a/package.py b/package.py index 1466031daa..b7f74e5126 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.0+dev" +version = "1.0.1" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 4a63529c67..afb48efec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.0+dev" +version = "1.0.1" description = "" authors = ["Ynput Team "] readme = "README.md" From d2cbdc1d2147a3fa134e651655b61ca3dc5131b4 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 3 Oct 2024 14:27:53 +0000 Subject: [PATCH 19/60] [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 e6d6c6f373..458129f367 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.1" +__version__ = "1.0.1+dev" diff --git a/package.py b/package.py index b7f74e5126..c059eed423 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.1" +version = "1.0.1+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index afb48efec3..0a7d0d76c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.1" +version = "1.0.1+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 820fd54a567881bc386a05e69986c7b33a6c9b12 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:39:10 +0200 Subject: [PATCH 20/60] remove pyblish exception logfrom records --- .../tools/publisher/models/publish.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/publish.py b/client/ayon_core/tools/publisher/models/publish.py index 6dfda38885..97a956b18f 100644 --- a/client/ayon_core/tools/publisher/models/publish.py +++ b/client/ayon_core/tools/publisher/models/publish.py @@ -32,17 +32,20 @@ PLUGIN_ORDER_OFFSET = 0.5 class MessageHandler(logging.Handler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.records = [] + self._records = [] def clear_records(self): - self.records = [] + self._records = [] def emit(self, record): try: record.msg = record.getMessage() except Exception: record.msg = str(record.msg) - self.records.append(record) + self._records.append(record) + + def get_records(self): + return self._records class PublishErrorInfo: @@ -1328,7 +1331,18 @@ class PublishModel: plugin, self._publish_context, instance ) if log_handler is not None: - result["records"] = log_handler.records + records = log_handler.get_records() + exception = result.get("error") + if exception is not None and records: + last_record = records[-1] + if ( + last_record.name == "pyblish.plugin" + and last_record.levelno == logging.ERROR + ): + # Remove last record made by pyblish + # - `log.exception(formatted_traceback)` + records.pop(-1) + result["records"] = records exception = result.get("error") if exception: From ceafd5b6d9e647246b71650d815f89f60828b4a4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 4 Oct 2024 02:11:42 +0200 Subject: [PATCH 21/60] Cinema4D: Open last workfile on launch --- client/ayon_core/hooks/pre_add_last_workfile_arg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hooks/pre_add_last_workfile_arg.py b/client/ayon_core/hooks/pre_add_last_workfile_arg.py index 74964e0df9..d5914c2352 100644 --- a/client/ayon_core/hooks/pre_add_last_workfile_arg.py +++ b/client/ayon_core/hooks/pre_add_last_workfile_arg.py @@ -28,7 +28,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "substancepainter", "aftereffects", "wrap", - "openrv" + "openrv", + "cinema4d" } launch_types = {LaunchTypes.local} From a40d8cc8172b23b641ef8e5d88a86576d3f646d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:14:03 +0200 Subject: [PATCH 22/60] added typehint imports --- client/ayon_core/pipeline/publish/publish_plugins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 3c2bafdba3..118596bffb 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -1,5 +1,7 @@ import inspect from abc import ABCMeta +import typing + import pyblish.api import pyblish.logic from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin @@ -16,6 +18,8 @@ from ayon_core.pipeline.colorspace import ( get_colorspace_settings_from_publish_context, set_colorspace_data_to_representation ) +if typing.TYPE_CHECKING: + from ayon_core.pipeline.create import CreateContext, CreatedInstance class AbstractMetaInstancePlugin(ABCMeta, MetaPlugin): From 8a3a1e8042ce047fe908d2a56d109e17401be7b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:14:12 +0200 Subject: [PATCH 23/60] moved imports a little --- client/ayon_core/pipeline/publish/publish_plugins.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 118596bffb..9124e8b763 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -5,8 +5,14 @@ import typing import pyblish.api import pyblish.logic from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin + from ayon_core.lib import BoolDef +from ayon_core.pipeline.colorspace import ( + get_colorspace_settings_from_publish_context, + set_colorspace_data_to_representation +) + from .lib import ( load_help_content_from_plugin, get_errored_instances_from_context, @@ -14,10 +20,6 @@ from .lib import ( get_instance_staging_dir, ) -from ayon_core.pipeline.colorspace import ( - get_colorspace_settings_from_publish_context, - set_colorspace_data_to_representation -) if typing.TYPE_CHECKING: from ayon_core.pipeline.create import CreateContext, CreatedInstance From fef043d3e293611988ebab9580c3f09a02c884bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:01:11 +0200 Subject: [PATCH 24/60] use imported classes for typehints --- .../pipeline/publish/publish_plugins.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 9124e8b763..d2c70894cc 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -1,6 +1,7 @@ import inspect from abc import ABCMeta import typing +from typing import Optional import pyblish.api import pyblish.logic @@ -133,7 +134,9 @@ class AYONPyblishPluginMixin: # callback(self) @classmethod - def register_create_context_callbacks(cls, create_context): + def register_create_context_callbacks( + cls, create_context: "CreateContext" + ): """Register callbacks for create context. It is possible to register callbacks listening to changes happened @@ -166,7 +169,7 @@ class AYONPyblishPluginMixin: return [] @classmethod - def get_attr_defs_for_context (cls, create_context): + def get_attr_defs_for_context(cls, create_context: "CreateContext"): """Publish attribute definitions for context. Attributes available for all families in plugin's `families` attribute. @@ -183,7 +186,9 @@ class AYONPyblishPluginMixin: return cls.get_attribute_defs() @classmethod - def instance_matches_plugin_families(cls, instance): + def instance_matches_plugin_families( + cls, instance: Optional["CreatedInstance"] + ): """Check if instance matches families. Args: @@ -207,7 +212,9 @@ class AYONPyblishPluginMixin: return False @classmethod - def get_attr_defs_for_instance(cls, create_context, instance): + def get_attr_defs_for_instance( + cls, create_context: "CreateContext", instance: "CreatedInstance" + ): """Publish attribute definitions for an instance. Attributes available for all families in plugin's `families` attribute. @@ -226,7 +233,9 @@ class AYONPyblishPluginMixin: return cls.get_attribute_defs() @classmethod - def convert_attribute_values(cls, create_context, instance): + def convert_attribute_values( + cls, create_context: "CreateContext", instance: "CreatedInstance" + ): """Convert attribute values for instance. Args: From d9e012c4af33fce61c41a0e67fb3d4439893887a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:12:43 +0200 Subject: [PATCH 25/60] fix key iteration Co-authored-by: Roy Nieterau --- client/ayon_core/tools/publisher/widgets/product_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py index f11dc90a5d..c2f1f24d2f 100644 --- a/client/ayon_core/tools/publisher/widgets/product_context.py +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -917,7 +917,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if instance_id not in self._current_instances_by_id: continue - for key, attr_name in ( + for key in ( "folderPath", "task", "variant", From b2b2ae6cfe369f15289ea09f4512531cf4a52fde Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:19:42 +0200 Subject: [PATCH 26/60] added some type hints --- client/ayon_core/lib/attribute_definitions.py | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 639778b16d..c565a00501 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -6,6 +6,7 @@ import json import copy import warnings from abc import ABCMeta, abstractmethod +from typing import Any, Optional import clique @@ -147,15 +148,15 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def __init__( self, - key, - default, - label=None, - tooltip=None, - is_label_horizontal=None, - visible=None, - enabled=None, - hidden=None, - disabled=None, + key: str, + default: Any, + label: Optional[str] = None, + tooltip: Optional[str] = None, + is_label_horizontal: Optional[bool] = None, + visible: Optional[bool] = None, + enabled: Optional[bool] = None, + hidden: Optional[bool] = None, + disabled: Optional[bool] = None, ): if is_label_horizontal is None: is_label_horizontal = True @@ -167,35 +168,35 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): visible, hidden, "visible", "hidden", True ) - self.key = key - self.label = label - self.tooltip = tooltip - self.default = default - self.is_label_horizontal = is_label_horizontal - self.visible = visible - self.enabled = enabled - self._id = uuid.uuid4().hex + self.key: str = key + self.label: Optional[str] = label + self.tooltip: Optional[str] = tooltip + self.default: Any = default + self.is_label_horizontal: bool = is_label_horizontal + self.visible: bool = visible + self.enabled: bool = enabled + self._id: str = uuid.uuid4().hex self.__init__class__ = AbstractAttrDef @property - def id(self): + def id(self) -> str: return self._id @property - def hidden(self): + def hidden(self) -> bool: return not self.visible @hidden.setter - def hidden(self, value): + def hidden(self, value: bool): self.visible = not value @property - def disabled(self): + def disabled(self) -> bool: return not self.enabled @disabled.setter - def disabled(self, value): + def disabled(self, value: bool): self.enabled = not value def __eq__(self, other): @@ -213,7 +214,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): @property @abstractmethod - def type(self): + def type(self) -> str: """Attribute definition type also used as identifier of class. Returns: @@ -286,7 +287,7 @@ class UILabelDef(UIDef): type = "label" def __init__(self, label, key=None): - super(UILabelDef, self).__init__(label=label, key=key) + super().__init__(label=label, key=key) def __eq__(self, other): if not super(UILabelDef, self).__eq__(other): @@ -309,7 +310,7 @@ class UnknownDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): kwargs["default"] = default - super(UnknownDef, self).__init__(key, **kwargs) + super().__init__(key, **kwargs) def convert_value(self, value): return value @@ -539,7 +540,7 @@ class EnumDef(AbstractAttrDef): return list(self._item_values.intersection(value)) def serialize(self): - data = super(EnumDef, self).serialize() + data = super().serialize() data["items"] = copy.deepcopy(self.items) data["multiselection"] = self.multiselection return data @@ -619,7 +620,7 @@ class BoolDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): if default is None: default = False - super(BoolDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) def convert_value(self, value): if isinstance(value, bool): From b8bc6ec2e3c33ec60246dbca7daa8218be9b8ab2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:19:56 +0200 Subject: [PATCH 27/60] simplified comparison --- client/ayon_core/lib/attribute_definitions.py | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index c565a00501..d5b9239809 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -199,18 +199,31 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def disabled(self, value: bool): self.enabled = not value - def __eq__(self, other): - if not isinstance(other, self.__class__): + def __eq__(self, other: Any) -> bool: + return self.compare_to_def(other) + + def __ne__(self, other: Any) -> bool: + return not self.compare_to_def(other) + + def compare_to_def( + self, + other: Any, + ignore_default: Optional[bool] = False, + ignore_enabled: Optional[bool] = False, + ignore_visible: Optional[bool] = False, + ) -> bool: + if not isinstance(other, self.__class__) or self.key != other.key: + return False + if not self._custom_def_compare(other): return False return ( - self.key == other.key - and self.default == other.default - and self.visible == other.visible - and self.enabled == other.enabled + (ignore_default or self.default == other.default) + and (ignore_visible or self.visible == other.visible) + and (ignore_enabled or self.enabled == other.enabled) ) - def __ne__(self, other): - return not self.__eq__(other) + def _custom_def_compare(self, other: "AbstractAttrDef") -> bool: + return True @property @abstractmethod @@ -289,9 +302,7 @@ class UILabelDef(UIDef): def __init__(self, label, key=None): super().__init__(label=label, key=key) - def __eq__(self, other): - if not super(UILabelDef, self).__eq__(other): - return False + def _custom_def_compare(self, other: "UILabelDef") -> bool: return self.label == other.label @@ -387,10 +398,7 @@ class NumberDef(AbstractAttrDef): self.maximum = maximum self.decimals = 0 if decimals is None else decimals - def __eq__(self, other): - if not super(NumberDef, self).__eq__(other): - return False - + def _custom_def_compare(self, other: "NumberDef") -> bool: return ( self.decimals == other.decimals and self.maximum == other.maximum @@ -457,10 +465,8 @@ class TextDef(AbstractAttrDef): self.placeholder = placeholder self.regex = regex - def __eq__(self, other): - if not super(TextDef, self).__eq__(other): - return False + def _custom_def_compare(self, other: "TextDef") -> bool: return ( self.multiline == other.multiline and self.regex == other.regex @@ -514,16 +520,13 @@ class EnumDef(AbstractAttrDef): elif default not in item_values: default = next(iter(item_values), None) - super(EnumDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) self.items = items self._item_values = item_values_set self.multiselection = multiselection - def __eq__(self, other): - if not super(EnumDef, self).__eq__(other): - return False - + def _custom_def_compare(self, other: "EnumDef") -> bool: return ( self.items == other.items and self.multiselection == other.multiselection From 116061aefbfa7e78c5128998ec98242f8d933038 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:25:19 +0200 Subject: [PATCH 28/60] added option to skip def specific comparison --- client/ayon_core/lib/attribute_definitions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index d5b9239809..99f9e9988f 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -211,10 +211,11 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): ignore_default: Optional[bool] = False, ignore_enabled: Optional[bool] = False, ignore_visible: Optional[bool] = False, + ignore_def_type_compare: Optional[bool] = False, ) -> bool: if not isinstance(other, self.__class__) or self.key != other.key: return False - if not self._custom_def_compare(other): + if not ignore_def_type_compare and not self._def_type_compare(other): return False return ( (ignore_default or self.default == other.default) @@ -222,7 +223,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): and (ignore_enabled or self.enabled == other.enabled) ) - def _custom_def_compare(self, other: "AbstractAttrDef") -> bool: + def _def_type_compare(self, other: "AbstractAttrDef") -> bool: return True @property @@ -398,7 +399,7 @@ class NumberDef(AbstractAttrDef): self.maximum = maximum self.decimals = 0 if decimals is None else decimals - def _custom_def_compare(self, other: "NumberDef") -> bool: + def _def_type_compare(self, other: "NumberDef") -> bool: return ( self.decimals == other.decimals and self.maximum == other.maximum @@ -465,8 +466,7 @@ class TextDef(AbstractAttrDef): self.placeholder = placeholder self.regex = regex - - def _custom_def_compare(self, other: "TextDef") -> bool: + def _def_type_compare(self, other: "TextDef") -> bool: return ( self.multiline == other.multiline and self.regex == other.regex @@ -526,7 +526,7 @@ class EnumDef(AbstractAttrDef): self._item_values = item_values_set self.multiselection = multiselection - def _custom_def_compare(self, other: "EnumDef") -> bool: + def _def_type_compare(self, other: "EnumDef") -> bool: return ( self.items == other.items and self.multiselection == other.multiselection From 0c801ba892a011cc2a2d0278886a2d1f0d609cf1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:25:32 +0200 Subject: [PATCH 29/60] fix ui label --- 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 99f9e9988f..01f5606f17 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -303,7 +303,7 @@ class UILabelDef(UIDef): def __init__(self, label, key=None): super().__init__(label=label, key=key) - def _custom_def_compare(self, other: "UILabelDef") -> bool: + def _def_type_compare(self, other: "UILabelDef") -> bool: return self.label == other.label From 88e4a159ca79424c156afd832d2f924985b2e46d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:25:50 +0200 Subject: [PATCH 30/60] fix clone --- client/ayon_core/lib/attribute_definitions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 01f5606f17..dd467797f1 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -183,6 +183,11 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def id(self) -> str: return self._id + def clone(self): + data = self.serialize() + data.pop("type") + return self.deserialize(data) + @property def hidden(self) -> bool: return not self.visible @@ -275,6 +280,9 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Data can be received using 'serialize' method. """ + if "type" in data: + data = dict(data) + data.pop("type") return cls(**data) From 5ca53978fcd8165fc24832e8da5b5edd70b3b5bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:26:07 +0200 Subject: [PATCH 31/60] added missing data to serialization o textdef --- client/ayon_core/lib/attribute_definitions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index dd467797f1..8ce270c218 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -488,6 +488,8 @@ class TextDef(AbstractAttrDef): def serialize(self): data = super(TextDef, self).serialize() data["regex"] = self.regex.pattern + data["multiline"] = self.multiline + data["placeholder"] = self.placeholder return data From 87a909ae40b497b03db01921aae2f695c14def86 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:26:17 +0200 Subject: [PATCH 32/60] use py3 super --- client/ayon_core/lib/attribute_definitions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 8ce270c218..6a0a10c349 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -295,7 +295,7 @@ class UIDef(AbstractAttrDef): is_value_def = False def __init__(self, key=None, default=None, *args, **kwargs): - super(UIDef, self).__init__(key, default, *args, **kwargs) + super().__init__(key, default, *args, **kwargs) def convert_value(self, value): return value @@ -401,7 +401,7 @@ class NumberDef(AbstractAttrDef): elif default > maximum: default = maximum - super(NumberDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) self.minimum = minimum self.maximum = maximum @@ -457,7 +457,7 @@ class TextDef(AbstractAttrDef): if default is None: default = "" - super(TextDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) if multiline is None: multiline = False @@ -486,7 +486,7 @@ class TextDef(AbstractAttrDef): return self.default def serialize(self): - data = super(TextDef, self).serialize() + data = super().serialize() data["regex"] = self.regex.pattern data["multiline"] = self.multiline data["placeholder"] = self.placeholder @@ -931,10 +931,10 @@ class FileDef(AbstractAttrDef): self.extensions = set(extensions) self.allow_sequences = allow_sequences self.extensions_label = extensions_label - super(FileDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) def __eq__(self, other): - if not super(FileDef, self).__eq__(other): + if not super().__eq__(other): return False return ( From a2170a76fe055e58d906fb75d056dc344eeb1160 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:26:41 +0200 Subject: [PATCH 33/60] initial idea of merging attributes --- .../tools/publisher/models/create.py | 79 ++++++++++++++++++- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 4b27081db2..577340d053 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -6,6 +6,7 @@ from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, deserialize_attr_defs, AbstractAttrDef, + EnumDef, ) from ayon_core.lib.profiles_filtering import filter_profiles from ayon_core.lib.attribute_definitions import UIDef @@ -296,6 +297,71 @@ class InstanceItem: ) +def _merge_attr_defs( + attr_def_src: AbstractAttrDef, attr_def_new: AbstractAttrDef +) -> Optional[AbstractAttrDef]: + if not attr_def_src.enabled and attr_def_new.enabled: + attr_def_src.enabled = True + if not attr_def_src.visible and attr_def_new.visible: + attr_def_src.visible = True + + if not isinstance(attr_def_src, EnumDef): + return None + if attr_def_src.items == attr_def_new.items: + return None + + src_item_values = { + item["value"] + for item in attr_def_src + } + for item in attr_def_new.items: + if item["value"] not in src_item_values: + attr_def_src.items.append(item) + + +def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]): + if not attr_defs: + return [] + if len(attr_defs) == 1: + return attr_defs[0] + + # Pop first and create clone of attribute definitions + defs_union: List[AbstractAttrDef] = [ + attr_def.clone() + for attr_def in attr_defs.pop(0) + ] + for instance_attr_defs in attr_defs: + idx = 0 + for attr_idx, attr_def in enumerate(instance_attr_defs): + is_enum = isinstance(attr_def, EnumDef) + match_idx = None + match_attr = None + for union_idx, union_def in enumerate(defs_union): + if ( + attr_def.compare_to_def( + union_def, + ignore_default=True, + ignore_enabled=True, + ignore_visible=True, + ignore_def_type_compare=is_enum + ) + ): + match_idx = union_idx + match_attr = union_def + break + + if match_attr is not None: + new_attr_def = _merge_attr_defs(match_attr, attr_def) + if new_attr_def is not None: + defs_union[match_idx] = new_attr_def + idx = match_idx + 1 + continue + + defs_union.insert(idx, attr_def.clone()) + idx += 1 + return defs_union + + class CreateModel: def __init__(self, controller: AbstractPublisherBackend): self._log = None @@ -729,9 +795,10 @@ class CreateModel: attr_defs = attr_val.attr_defs if not attr_defs: continue - - if plugin_name not in all_defs_by_plugin_name: - all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs + 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, {}) @@ -744,6 +811,10 @@ class CreateModel: value = attr_val[attr_def.key] attr_values.append((item_id, value)) + attr_defs_by_plugin_name = {} + for plugin_name, attr_defs in all_defs_by_plugin_name.items(): + attr_defs_by_plugin_name[plugin_name] = merge_attr_defs(attr_defs) + output = [] for plugin in self._create_context.plugins_with_defs: plugin_name = plugin.__name__ @@ -751,7 +822,7 @@ class CreateModel: continue output.append(( plugin_name, - all_defs_by_plugin_name[plugin_name], + attr_defs_by_plugin_name[plugin_name], all_plugin_values )) return output From b0001f9a5886a4e658bc47a7fd828bcd50f0eee1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:32:47 +0200 Subject: [PATCH 34/60] fix values loop --- 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 577340d053..536e1475ea 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -312,7 +312,7 @@ def _merge_attr_defs( src_item_values = { item["value"] - for item in attr_def_src + for item in attr_def_src.items } for item in attr_def_new.items: if item["value"] not in src_item_values: From ed36077292d28ea3fee027f1545b4fd3289a2360 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:17:16 +0200 Subject: [PATCH 35/60] added 'is_value_valid' implementation for attribute definitions --- client/ayon_core/lib/attribute_definitions.py | 117 +++++++++++++++--- 1 file changed, 98 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 6a0a10c349..4877a45118 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -228,8 +228,21 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): and (ignore_enabled or self.enabled == other.enabled) ) - def _def_type_compare(self, other: "AbstractAttrDef") -> bool: - return True + @abstractmethod + def is_value_valid(self, value: Any) -> bool: + """Check if value is valid. + + This should return False if value is not valid based + on definition type. + + Args: + value (Any): Value to validate based on definition type. + + Returns: + bool: True if value is valid. + + """ + pass @property @abstractmethod @@ -286,6 +299,9 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return cls(**data) + def _def_type_compare(self, other: "AbstractAttrDef") -> bool: + return True + # ----------------------------------------- # UI attribute definitions won't hold value @@ -297,6 +313,9 @@ class UIDef(AbstractAttrDef): def __init__(self, key=None, default=None, *args, **kwargs): super().__init__(key, default, *args, **kwargs) + def is_value_valid(self, value: Any) -> bool: + return True + def convert_value(self, value): return value @@ -332,6 +351,9 @@ class UnknownDef(AbstractAttrDef): kwargs["default"] = default super().__init__(key, **kwargs) + def is_value_valid(self, value: Any) -> bool: + return True + def convert_value(self, value): return value @@ -352,6 +374,9 @@ class HiddenDef(AbstractAttrDef): kwargs["visible"] = False super().__init__(key, **kwargs) + def is_value_valid(self, value: Any) -> bool: + return True + def convert_value(self, value): return value @@ -407,12 +432,15 @@ class NumberDef(AbstractAttrDef): self.maximum = maximum self.decimals = 0 if decimals is None else decimals - def _def_type_compare(self, other: "NumberDef") -> bool: - return ( - self.decimals == other.decimals - and self.maximum == other.maximum - and self.maximum == other.maximum - ) + def is_value_valid(self, value: Any) -> bool: + if self.decimals == 0: + if not isinstance(value, int): + return False + elif not isinstance(value, float): + return False + if self.minimum > value > self.maximum: + return False + return True def convert_value(self, value): if isinstance(value, str): @@ -428,6 +456,13 @@ class NumberDef(AbstractAttrDef): return int(value) return round(float(value), self.decimals) + def _def_type_compare(self, other: "NumberDef") -> bool: + return ( + self.decimals == other.decimals + and self.maximum == other.maximum + and self.maximum == other.maximum + ) + class TextDef(AbstractAttrDef): """Text definition. @@ -474,11 +509,12 @@ class TextDef(AbstractAttrDef): self.placeholder = placeholder self.regex = regex - def _def_type_compare(self, other: "TextDef") -> bool: - return ( - self.multiline == other.multiline - and self.regex == other.regex - ) + def is_value_valid(self, value: Any) -> bool: + if not isinstance(value, str): + return False + if self.regex and not self.regex.match(value): + return False + return True def convert_value(self, value): if isinstance(value, str): @@ -492,6 +528,12 @@ class TextDef(AbstractAttrDef): data["placeholder"] = self.placeholder return data + def _def_type_compare(self, other: "TextDef") -> bool: + return ( + self.multiline == other.multiline + and self.regex == other.regex + ) + class EnumDef(AbstractAttrDef): """Enumeration of items. @@ -536,12 +578,6 @@ class EnumDef(AbstractAttrDef): self._item_values = item_values_set self.multiselection = multiselection - def _def_type_compare(self, other: "EnumDef") -> bool: - return ( - self.items == other.items - and self.multiselection == other.multiselection - ) - def convert_value(self, value): if not self.multiselection: if value in self._item_values: @@ -552,6 +588,17 @@ class EnumDef(AbstractAttrDef): return copy.deepcopy(self.default) return list(self._item_values.intersection(value)) + def is_value_valid(self, value: Any) -> bool: + """Check if item is available in possible values.""" + if isinstance(value, list): + if not self.multiselection: + return False + return all(value in self._item_values for value in value) + + if self.multiselection: + return False + return value in self._item_values + def serialize(self): data = super().serialize() data["items"] = copy.deepcopy(self.items) @@ -620,6 +667,12 @@ class EnumDef(AbstractAttrDef): return output + def _def_type_compare(self, other: "EnumDef") -> bool: + return ( + self.items == other.items + and self.multiselection == other.multiselection + ) + class BoolDef(AbstractAttrDef): """Boolean representation. @@ -635,6 +688,9 @@ class BoolDef(AbstractAttrDef): default = False super().__init__(key, default=default, **kwargs) + def is_value_valid(self, value: Any) -> bool: + return isinstance(value, bool) + def convert_value(self, value): if isinstance(value, bool): return value @@ -944,6 +1000,29 @@ class FileDef(AbstractAttrDef): and self.allow_sequences == other.allow_sequences ) + def is_value_valid(self, value: Any) -> bool: + if self.single_item: + if not isinstance(value, dict): + return False + try: + FileDefItem.from_dict(value) + return True + except (ValueError, KeyError): + return False + + if not isinstance(value, list): + return False + + for item in value: + if not isinstance(item, dict): + return False + + try: + FileDefItem.from_dict(item) + except (ValueError, KeyError): + return False + return True + def convert_value(self, value): if isinstance(value, (str, dict)): value = [value] From a9e58c40477d374d4ba5935f91757f8b82f71b46 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:17:27 +0200 Subject: [PATCH 36/60] added method to get attribute definition --- client/ayon_core/pipeline/create/structures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 8594d82848..bcc9a87c49 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -148,6 +148,9 @@ class AttributeValues: for key in self._attr_defs_by_key.keys(): yield key, self._data.get(key) + def get_attr_def(self, key, default=None): + return self._attr_defs_by_key.get(key, default) + def update(self, value): changes = {} for _key, _value in dict(value).items(): From 87329306d4a791b90e540ec49054ce187aa6d2df Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:17:45 +0200 Subject: [PATCH 37/60] don't set all values unless they are valid for the instance --- .../tools/publisher/models/create.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 536e1475ea..493fcc3b01 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -333,10 +333,18 @@ def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]): for instance_attr_defs in attr_defs: idx = 0 for attr_idx, attr_def in enumerate(instance_attr_defs): + # QUESTION should we merge NumberDef too? Use lowest min and + # biggest max... is_enum = isinstance(attr_def, EnumDef) match_idx = None match_attr = None for union_idx, union_def in enumerate(defs_union): + if is_enum and ( + not isinstance(union_def, EnumDef) + or union_def.multiselection != attr_def.multiselection + ): + continue + if ( attr_def.compare_to_def( union_def, @@ -759,6 +767,18 @@ class CreateModel: 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 + plugin_val[key] = value def get_publish_attribute_definitions( From c3aff4deb2751796178ee94940abf437658bec13 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:21:16 +0200 Subject: [PATCH 38/60] implemented similar logic to create attributes --- client/ayon_core/tools/publisher/models/create.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 493fcc3b01..f2a7901e42 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -717,8 +717,16 @@ class CreateModel: for instance_id in instance_ids: instance = self._get_instance_by_id(instance_id) creator_attributes = instance["creator_attributes"] - if key in creator_attributes: - creator_attributes[key] = value + 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 def get_creator_attribute_definitions( self, instance_ids: List[str] From e2eb8260deff0b5b108f4a613f97d426a422b6e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:41:07 +0200 Subject: [PATCH 39/60] fix validation of context in main window --- client/ayon_core/tools/publisher/control.py | 22 ++++++++++++++++++- .../tools/publisher/models/create.py | 22 ++++++++++++++++++- client/ayon_core/tools/publisher/window.py | 19 ++++++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 43b491a20f..51eaefe0e0 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -35,7 +35,27 @@ class PublisherController( Known topics: "show.detailed.help" - Detailed help requested (UI related). "show.card.message" - Show card message request (UI related). - "instances.refresh.finished" - Instances are refreshed. + # --- Create model --- + "create.model.reset" - Reset of create model. + "instances.create.failed" - Creation failed. + "convertors.convert.failed" - Convertor failed. + "instances.save.failed" - Save failed. + "instance.thumbnail.changed" - Thumbnail changed. + "instances.collection.failed" - Collection of instances failed. + "convertors.find.failed" - Convertor find failed. + "instances.create.failed" - Create instances failed. + "instances.remove.failed" - Remove instances failed. + "create.context.added.instance" - Create instance added to context. + "create.context.value.changed" - Create instance or context value + changed. + "create.context.pre.create.attrs.changed" - Pre create attributes + changed. + "create.context.create.attrs.changed" - Create attributes changed. + "create.context.publish.attrs.changed" - Publish attributes changed. + "create.context.removed.instance" - Instance removed from context. + "create.model.instances.context.changed" - Instances changed context. + like folder, task or variant. + # --- Publish model --- "plugins.refresh.finished" - Plugins refreshed. "publish.reset.finished" - Reset finished. "controller.reset.started" - Controller reset started. diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index f2a7901e42..a08a3c6863 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -371,6 +371,8 @@ def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]): class CreateModel: + _CONTEXT_KEYS = {"folderPath", "task", "variant", "productName"} + def __init__(self, controller: AbstractPublisherBackend): self._log = None self._controller: AbstractPublisherBackend = controller @@ -527,6 +529,12 @@ class CreateModel: instance = self._get_instance_by_id(instance_id) for key, value in changes.items(): instance[key] = value + self._emit_event( + "create.model.instances.context.changed", + { + "instance_ids": list(changes_by_instance_id.keys()) + } + ) def get_convertor_items(self) -> Dict[str, ConvertorItem]: return self._create_context.convertor_items_by_id @@ -1032,16 +1040,28 @@ class CreateModel: return instance_changes = {} + context_changed_ids = set() for item in event.data["changes"]: instance_id = None if item["instance"]: instance_id = item["instance"].id - instance_changes[instance_id] = item["changes"] + changes = item["changes"] + instance_changes[instance_id] = changes + if instance_id is None: + continue + + if self._CONTEXT_KEYS.intersection(set(changes)): + context_changed_ids.add(instance_id) self._emit_event( "create.context.value.changed", {"instance_changes": instance_changes}, ) + if context_changed_ids: + self._emit_event( + "create.model.instances.context.changed", + {"instance_ids": list(context_changed_ids)}, + ) def _cc_pre_create_attr_changed(self, event): identifiers = event["identifiers"] diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index e4da71b3d6..1d16159ffa 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -281,7 +281,19 @@ class PublisherWindow(QtWidgets.QDialog): ) controller.register_event_callback( - "instances.refresh.finished", self._on_instances_refresh + "create.model.reset", self._on_create_model_reset + ) + controller.register_event_callback( + "create.context.added.instance", + self._event_callback_validate_instances + ) + controller.register_event_callback( + "create.context.removed.instance", + self._event_callback_validate_instances + ) + controller.register_event_callback( + "create.model.instances.context.changed", + self._event_callback_validate_instances ) controller.register_event_callback( "publish.reset.finished", self._on_publish_reset @@ -936,13 +948,16 @@ class PublisherWindow(QtWidgets.QDialog): self._set_footer_enabled(bool(all_valid)) - def _on_instances_refresh(self): + def _on_create_model_reset(self): self._validate_create_instances() context_title = self._controller.get_context_title() self.set_context_label(context_title) self._update_publish_details_widget() + def _event_callback_validate_instances(self, _event): + self._validate_create_instances() + def _set_comment_input_visiblity(self, visible): self._comment_input.setVisible(visible) self._footer_spacer.setVisible(not visible) From 36541c5aae0023f479f343521fc25ee0e7010134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:45:41 +0200 Subject: [PATCH 40/60] :art: add unreal to hosts unreal can do local rendering/publishing and without it, it is missing thumbnail. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 4ffabf6028..37bbac8898 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -36,7 +36,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "traypublisher", "substancepainter", "nuke", - "aftereffects" + "aftereffects", + "unreal" ] enabled = False From 047cc8e0cf90d367f683739642ea25c131af2fe2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:01:20 +0200 Subject: [PATCH 41/60] fix issues in list view widgets --- .../tools/publisher/widgets/list_view_widgets.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 14814a4aa6..a6a2f08752 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -110,7 +110,7 @@ class ListItemDelegate(QtWidgets.QStyledItemDelegate): class InstanceListItemWidget(QtWidgets.QWidget): """Widget with instance info drawn over delegate paint. - This is required to be able use custom checkbox on custom place. + This is required to be able to use custom checkbox on custom place. """ active_changed = QtCore.Signal(str, bool) double_clicked = QtCore.Signal() @@ -245,8 +245,8 @@ class ListContextWidget(QtWidgets.QFrame): class InstanceListGroupWidget(QtWidgets.QFrame): """Widget representing group of instances. - Has collapse/expand indicator, label of group and checkbox modifying all of - it's children. + Has collapse/expand indicator, label of group and checkbox modifying all + of its children. """ expand_changed = QtCore.Signal(str, bool) toggle_requested = QtCore.Signal(str, int) @@ -392,7 +392,7 @@ class InstanceTreeView(QtWidgets.QTreeView): def _mouse_press(self, event): """Store index of pressed group. - This is to be able change state of group and process mouse + This is to be able to change state of group and process mouse "double click" as 2x "single click". """ if event.button() != QtCore.Qt.LeftButton: @@ -588,7 +588,7 @@ class InstanceListView(AbstractInstanceView): # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() - for instance in self._controller.get_instances(): + for instance in self._controller.get_instance_items(): group_label = instance.group_label group_names.add(group_label) instances_by_group_name[group_label].append(instance) @@ -612,7 +612,7 @@ class InstanceListView(AbstractInstanceView): # Mapping of existing instances under group item existing_mapping = {} - # Get group index to be able get children indexes + # Get group index to be able to get children indexes group_index = self._instance_model.index( group_item.row(), group_item.column() ) From 5f380d244b9f86c9d99b2af444db8da3f6f2c921 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:29:55 +0200 Subject: [PATCH 42/60] consider active as context change --- client/ayon_core/tools/publisher/models/create.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index a08a3c6863..a68b5e2879 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -371,7 +371,13 @@ def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]): class CreateModel: - _CONTEXT_KEYS = {"folderPath", "task", "variant", "productName"} + _CONTEXT_KEYS = { + "active", + "folderPath", + "task", + "variant", + "productName", + } def __init__(self, controller: AbstractPublisherBackend): self._log = None From cc3aa6936eccb21d5dc3e5428a54a5bd92853680 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:30:06 +0200 Subject: [PATCH 43/60] remove unnecessary callbacks --- client/ayon_core/tools/publisher/window.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 1d16159ffa..a912495d4e 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -253,12 +253,6 @@ class PublisherWindow(QtWidgets.QDialog): help_btn.clicked.connect(self._on_help_click) tabs_widget.tab_changed.connect(self._on_tab_change) - overview_widget.active_changed.connect( - self._on_context_or_active_change - ) - overview_widget.instance_context_changed.connect( - self._on_context_or_active_change - ) overview_widget.create_requested.connect( self._on_create_request ) From a21341ad1588f53ab74789cd84f4833580045238 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:43:38 +0200 Subject: [PATCH 44/60] views are propagating context changes --- .../publisher/widgets/card_view_widgets.py | 36 ++++++++++++------- .../publisher/widgets/list_view_widgets.py | 19 +++++++--- .../publisher/widgets/overview_widget.py | 14 ++++---- .../publisher/widgets/product_context.py | 3 -- .../tools/publisher/widgets/product_info.py | 12 +++---- 5 files changed, 49 insertions(+), 35 deletions(-) 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 6ef34b86f8..67793bb50e 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -22,6 +22,7 @@ Only one item can be selected at a time. import re import collections +from typing import Dict from qtpy import QtWidgets, QtCore @@ -217,11 +218,18 @@ class InstanceGroupWidget(BaseGroupWidget): def update_icons(self, group_icons): self._group_icons = group_icons - def update_instance_values(self, context_info_by_id): + def update_instance_values( + self, context_info_by_id, instance_items_by_id, instance_ids + ): """Trigger update on instance widgets.""" for instance_id, widget in self._widgets_by_id.items(): - widget.update_instance_values(context_info_by_id[instance_id]) + if instance_ids is not None and instance_id not in instance_ids: + continue + widget.update_instance( + instance_items_by_id[instance_id], + context_info_by_id[instance_id] + ) def update_instances(self, instances, context_info_by_id): """Update instances for the group. @@ -391,9 +399,6 @@ class ConvertorItemCardWidget(CardWidget): self._icon_widget = icon_widget self._label_widget = label_widget - def update_instance_values(self, context_info): - pass - class InstanceCardWidget(CardWidget): """Card widget representing instance.""" @@ -461,7 +466,7 @@ class InstanceCardWidget(CardWidget): self._active_checkbox = active_checkbox self._expand_btn = expand_btn - self.update_instance_values(context_info) + self._update_instance_context(context_info) def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) @@ -486,7 +491,7 @@ class InstanceCardWidget(CardWidget): def update_instance(self, instance, context_info): """Update instance object and update UI.""" self.instance = instance - self.update_instance_values(context_info) + self._update_instance_context(context_info) def _validate_context(self, context_info): valid = context_info.is_valid @@ -522,7 +527,7 @@ class InstanceCardWidget(CardWidget): QtCore.Qt.NoTextInteraction ) - def update_instance_values(self, context_info): + def _update_instance_context(self, context_info): """Update instance data""" self._update_product_name() self.set_active(self.instance.is_active) @@ -596,7 +601,7 @@ class InstanceCardView(AbstractInstanceView): self._context_widget = None self._convertor_items_group = None self._active_toggle_enabled = True - self._widgets_by_group = {} + self._widgets_by_group: Dict[str, InstanceGroupWidget] = {} self._ordered_groups = [] self._explicitly_selected_instance_ids = [] @@ -702,7 +707,7 @@ class InstanceCardView(AbstractInstanceView): # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) identifiers_by_group = collections.defaultdict(set) - for instance in self._controller.get_instances(): + for instance in self._controller.get_instance_items(): group_name = instance.group_label instances_by_group[group_name].append(instance) identifiers_by_group[group_name].add( @@ -817,11 +822,18 @@ class InstanceCardView(AbstractInstanceView): self._convertor_items_group.update_items(convertor_items) - def refresh_instance_states(self): + def refresh_instance_states(self, instance_ids=None): """Trigger update of instances on group widgets.""" + if instance_ids is not None: + instance_ids = set(instance_ids) context_info_by_id = self._controller.get_instances_context_info() + instance_items_by_id = self._controller.get_instance_items_by_id( + instance_ids + ) for widget in self._widgets_by_group.values(): - widget.update_instance_values(context_info_by_id) + widget.update_instance_values( + context_info_by_id, instance_items_by_id, instance_ids + ) 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 a6a2f08752..a8144e71f4 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -191,9 +191,9 @@ class InstanceListItemWidget(QtWidgets.QWidget): def update_instance(self, instance, context_info): """Update instance object.""" self.instance = instance - self.update_instance_values(context_info) + self._update_instance_values(context_info) - def update_instance_values(self, context_info): + def _update_instance_values(self, context_info): """Update instance data propagated to widgets.""" # Check product name label = self.instance.label @@ -873,12 +873,21 @@ class InstanceListView(AbstractInstanceView): widget = self._group_widgets.pop(group_name) widget.deleteLater() - def refresh_instance_states(self): + def refresh_instance_states(self, instance_ids=None): """Trigger update of all instances.""" + if instance_ids is not None: + instance_ids = set(instance_ids) context_info_by_id = self._controller.get_instances_context_info() + instance_items_by_id = self._controller.get_instance_items_by_id( + instance_ids + ) for instance_id, widget in self._widgets_by_id.items(): - context_info = context_info_by_id[instance_id] - widget.update_instance_values(context_info) + if instance_ids is not None and instance_id not in instance_ids: + continue + widget.update_instance( + instance_items_by_id[instance_id], + context_info_by_id[instance_id], + ) 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/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index beefa1ca98..5e8b803fc3 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -16,7 +16,6 @@ from .product_info import ProductInfoWidget class OverviewWidget(QtWidgets.QFrame): active_changed = QtCore.Signal() - instance_context_changed = QtCore.Signal() create_requested = QtCore.Signal() convert_requested = QtCore.Signal() publish_tab_requested = QtCore.Signal() @@ -134,9 +133,6 @@ class OverviewWidget(QtWidgets.QFrame): self._on_active_changed ) # Instance context has changed - product_attributes_widget.instance_context_changed.connect( - self._on_instance_context_change - ) product_attributes_widget.convert_requested.connect( self._on_convert_requested ) @@ -163,6 +159,10 @@ class OverviewWidget(QtWidgets.QFrame): "create.context.removed.instance", self._on_instances_removed ) + controller.register_event_callback( + "create.model.instances.context.changed", + self._on_instance_context_change + ) self._product_content_widget = product_content_widget self._product_content_layout = product_content_layout @@ -362,7 +362,7 @@ class OverviewWidget(QtWidgets.QFrame): self._current_state == "publish" ) - def _on_instance_context_change(self): + def _on_instance_context_change(self, event): current_idx = self._product_views_layout.currentIndex() for idx in range(self._product_views_layout.count()): if idx == current_idx: @@ -372,9 +372,7 @@ class OverviewWidget(QtWidgets.QFrame): widget.set_refreshed(False) current_widget = self._product_views_layout.widget(current_idx) - current_widget.refresh_instance_states() - - self.instance_context_changed.emit() + current_widget.refresh_instance_states(event["instance_ids"]) def _on_convert_requested(self): self.convert_requested.emit() diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py index c2f1f24d2f..04c9ca7e56 100644 --- a/client/ayon_core/tools/publisher/widgets/product_context.py +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -621,7 +621,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget): product name: [ immutable ] [Submit] [Cancel] """ - instance_context_changed = QtCore.Signal() multiselection_text = "< Multiselection >" unknown_value = "N/A" @@ -775,7 +774,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self._controller.set_instances_context_info(changes_by_id) self._refresh_items() - self.instance_context_changed.emit() def _on_cancel(self): """Cancel changes and set back to their irigin value.""" @@ -933,4 +931,3 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if changed: self._refresh_items() self._refresh_content() - self.instance_context_changed.emit() diff --git a/client/ayon_core/tools/publisher/widgets/product_info.py b/client/ayon_core/tools/publisher/widgets/product_info.py index 9a7700d73d..27b7aacf38 100644 --- a/client/ayon_core/tools/publisher/widgets/product_info.py +++ b/client/ayon_core/tools/publisher/widgets/product_info.py @@ -26,7 +26,6 @@ class ProductInfoWidget(QtWidgets.QWidget): │ │ attributes │ └───────────────────────────────┘ """ - instance_context_changed = QtCore.Signal() convert_requested = QtCore.Signal() def __init__( @@ -123,13 +122,14 @@ class ProductInfoWidget(QtWidgets.QWidget): self._context_selected = False self._all_instances_valid = True - global_attrs_widget.instance_context_changed.connect( - self._on_instance_context_changed - ) convert_btn.clicked.connect(self._on_convert_click) thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) + controller.register_event_callback( + "create.model.instances.context.changed", + self._on_instance_context_change + ) controller.register_event_callback( "instance.thumbnail.changed", self._on_thumbnail_changed @@ -196,7 +196,7 @@ class ProductInfoWidget(QtWidgets.QWidget): self._update_thumbnails() - def _on_instance_context_changed(self): + def _on_instance_context_change(self): instance_ids = { instance.id for instance in self._current_instances @@ -214,8 +214,6 @@ class ProductInfoWidget(QtWidgets.QWidget): self.creator_attrs_widget.set_instances_valid(all_valid) self.publish_attrs_widget.set_instances_valid(all_valid) - self.instance_context_changed.emit() - def _on_convert_click(self): self.convert_requested.emit() From 22c3142894ac83af93e72c990f79313c8163060b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:05:59 +0200 Subject: [PATCH 45/60] added helper method to change active state --- client/ayon_core/tools/publisher/abstract.py | 7 ++++ client/ayon_core/tools/publisher/control.py | 3 ++ .../tools/publisher/models/create.py | 35 +++++++++++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 3a968eee28..4787e8a21b 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -3,6 +3,7 @@ from typing import ( Optional, Dict, List, + Set, Tuple, Any, Callable, @@ -353,6 +354,12 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ): pass + @abstractmethod + def set_instances_active_state( + self, active_state_by_id: Dict[str, bool] + ): + 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 51eaefe0e0..347755d557 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -220,6 +220,9 @@ class PublisherController( changes_by_instance_id ) + def set_instances_active_state(self, active_state_by_id): + self._create_model.set_instances_active_state(active_state_by_id) + 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 a68b5e2879..2aa7b169a0 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -1,6 +1,16 @@ import logging import re -from typing import Union, List, Dict, Tuple, Any, Optional, Iterable, Pattern +from typing import ( + Union, + List, + Dict, + Set, + Tuple, + Any, + Optional, + Iterable, + Pattern, +) from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, @@ -542,6 +552,21 @@ class CreateModel: } ) + def set_instances_active_state( + self, active_state_by_id: Dict[str, bool] + ): + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id, active in active_state_by_id.items(): + instance = self._create_context.get_instance_by_id(instance_id) + instance["active"] = active + + self._emit_event( + "create.model.instances.context.changed", + { + "instance_ids": set(active_state_by_id.keys()) + } + ) + def get_convertor_items(self) -> Dict[str, ConvertorItem]: return self._create_context.convertor_items_by_id @@ -896,8 +921,12 @@ class CreateModel: } ) - def _emit_event(self, topic: str, data: Optional[Dict[str, Any]] = None): - self._controller.emit_event(topic, data) + def _emit_event( + self, + topic: str, + data: Optional[Dict[str, Any]] = None + ): + self._controller.emit_event(topic, data, CREATE_EVENT_SOURCE) def _get_current_project_settings(self) -> Dict[str, Any]: """Current project settings. From c3641b380dd0dd6bac41b6e983faf0d551f1f41b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:11:04 +0200 Subject: [PATCH 46/60] card view widget is able to change active state --- .../publisher/widgets/card_view_widgets.py | 49 +++++++++---------- .../publisher/widgets/overview_widget.py | 13 ----- .../tools/publisher/widgets/widgets.py | 1 - 3 files changed, 22 insertions(+), 41 deletions(-) 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 67793bb50e..095a4eae7c 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -315,8 +315,9 @@ class CardWidget(BaseClickableFrame): def set_selected(self, selected): """Set card as selected.""" - if selected == self._selected: + if selected is self._selected: return + self._selected = selected state = "selected" if selected else "" self.setProperty("state", state) @@ -466,7 +467,7 @@ class InstanceCardWidget(CardWidget): self._active_checkbox = active_checkbox self._expand_btn = expand_btn - self._update_instance_context(context_info) + self._update_instance_values(context_info) def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) @@ -475,23 +476,16 @@ class InstanceCardWidget(CardWidget): def is_active(self): return self._active_checkbox.isChecked() - def set_active(self, new_value): + def _set_active(self, new_value): """Set instance as active.""" checkbox_value = self._active_checkbox.isChecked() - instance_value = self.instance.is_active - - # First change instance value and them change checkbox - # - prevent to trigger `active_changed` signal - if instance_value != new_value: - self.instance.is_active = new_value - if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) def update_instance(self, instance, context_info): """Update instance object and update UI.""" self.instance = instance - self._update_instance_context(context_info) + self._update_instance_values(context_info) def _validate_context(self, context_info): valid = context_info.is_valid @@ -527,10 +521,10 @@ class InstanceCardWidget(CardWidget): QtCore.Qt.NoTextInteraction ) - def _update_instance_context(self, context_info): + def _update_instance_values(self, context_info): """Update instance data""" self._update_product_name() - self.set_active(self.instance.is_active) + self._set_active(self.instance.is_active) self._validate_context(context_info) def _set_expanded(self, expanded=None): @@ -544,7 +538,6 @@ class InstanceCardWidget(CardWidget): if new_value == old_value: return - self.instance.is_active = new_value self.active_changed.emit(self._id, new_value) def _on_expend_clicked(self): @@ -630,24 +623,25 @@ class InstanceCardView(AbstractInstanceView): return widgets = self._get_selected_widgets() - changed = False + active_state_by_id = {} for widget in widgets: if not isinstance(widget, InstanceCardWidget): continue + instance_id = widget.id is_active = widget.is_active if value == -1: - widget.set_active(not is_active) - changed = True + active_state_by_id[instance_id] = not is_active continue _value = bool(value) if is_active is not _value: - widget.set_active(_value) - changed = True + active_state_by_id[instance_id] = _value - if changed: - self.active_changed.emit() + if not active_state_by_id: + return + + self._controller.set_instances_active_state(active_state_by_id) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Space: @@ -838,14 +832,15 @@ class InstanceCardView(AbstractInstanceView): def _on_active_changed(self, group_name, instance_id, value): group_widget = self._widgets_by_group[group_name] instance_widget = group_widget.get_widget_by_item_id(instance_id) - if instance_widget.is_selected: + active_state_by_id = {} + if not instance_widget.is_selected: + active_state_by_id[instance_id] = value + else: for widget in self._get_selected_widgets(): if isinstance(widget, InstanceCardWidget): - widget.set_active(value) - else: - self._select_item_clear(instance_id, group_name, instance_widget) - self.selection_changed.emit() - self.active_changed.emit() + active_state_by_id[widget.id] = value + + self._controller.set_instances_active_state(active_state_by_id) def _on_widget_selection(self, instance_id, group_name, selection_type): """Select specific item by instance id. diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index 5e8b803fc3..a09ee80ed5 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -15,7 +15,6 @@ from .product_info import ProductInfoWidget class OverviewWidget(QtWidgets.QFrame): - active_changed = QtCore.Signal() create_requested = QtCore.Signal() convert_requested = QtCore.Signal() publish_tab_requested = QtCore.Signal() @@ -125,13 +124,6 @@ class OverviewWidget(QtWidgets.QFrame): product_view_cards.double_clicked.connect( self.publish_tab_requested ) - # Active instances changed - product_list_view.active_changed.connect( - self._on_active_changed - ) - product_view_cards.active_changed.connect( - self._on_active_changed - ) # Instance context has changed product_attributes_widget.convert_requested.connect( self._on_convert_requested @@ -312,11 +304,6 @@ class OverviewWidget(QtWidgets.QFrame): instances, context_selected, convertor_identifiers ) - def _on_active_changed(self): - if self._refreshing_instances: - return - self.active_changed.emit() - def _on_change_anim(self, value): self._create_widget.setVisible(True) self._product_attributes_wrap.setVisible(True) diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 00c87ac249..a9d34c4c66 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -298,7 +298,6 @@ class ChangeViewBtn(PublishIconBtn): class AbstractInstanceView(QtWidgets.QWidget): """Abstract class for instance view in creation part.""" selection_changed = QtCore.Signal() - active_changed = QtCore.Signal() # Refreshed attribute is not changed by view itself # - widget which triggers `refresh` is changing the state # TODO store that information in widget which cares about refreshing From 8b5e3e7d77b4933dbe5b00b3101adade2a077f98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:54:34 +0200 Subject: [PATCH 47/60] implemented active state in list view --- .../publisher/widgets/list_view_widgets.py | 63 +++++++------------ 1 file changed, 24 insertions(+), 39 deletions(-) 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 a8144e71f4..bc3353ba5e 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -118,7 +118,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): def __init__(self, instance, context_info, parent): super().__init__(parent) - self.instance = instance + self._instance_id = instance.id instance_label = instance.label if instance_label is None: @@ -171,47 +171,34 @@ class InstanceListItemWidget(QtWidgets.QWidget): def is_active(self): """Instance is activated.""" - return self.instance.is_active + return self._active_checkbox.isChecked() def set_active(self, new_value): """Change active state of instance and checkbox.""" - checkbox_value = self._active_checkbox.isChecked() - instance_value = self.instance.is_active + old_value = self.is_active() if new_value is None: - new_value = not instance_value + new_value = not old_value - # First change instance value and them change checkbox - # - prevent to trigger `active_changed` signal - if instance_value != new_value: - self.instance.is_active = new_value - - if checkbox_value != new_value: + if new_value != old_value: + self._active_checkbox.blockSignals(True) self._active_checkbox.setChecked(new_value) + self._active_checkbox.blockSignals(False) def update_instance(self, instance, context_info): """Update instance object.""" - self.instance = instance - self._update_instance_values(context_info) - - def _update_instance_values(self, context_info): - """Update instance data propagated to widgets.""" # Check product name - label = self.instance.label + label = instance.label if label != self._instance_label_widget.text(): self._instance_label_widget.setText(html_escape(label)) # Check active state - self.set_active(self.instance.is_active) + self.set_active(instance.is_active) # Check valid states self._set_valid_property(context_info.is_valid) def _on_active_change(self): - new_value = self._active_checkbox.isChecked() - old_value = self.instance.is_active - if new_value == old_value: - return - - self.instance.is_active = new_value - self.active_changed.emit(self.instance.id, new_value) + self.active_changed.emit( + self._instance_id, self._active_checkbox.isChecked() + ) def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) @@ -892,20 +879,21 @@ class InstanceListView(AbstractInstanceView): def _on_active_changed(self, changed_instance_id, new_value): selected_instance_ids, _, _ = self.get_selected_items() - selected_ids = set() + active_by_id = {} found = False for instance_id in selected_instance_ids: - selected_ids.add(instance_id) + active_by_id[instance_id] = new_value if not found and instance_id == changed_instance_id: found = True if not found: - selected_ids = set() - selected_ids.add(changed_instance_id) + active_by_id = {changed_instance_id: new_value} - self._change_active_instances(selected_ids, new_value) + self._controller.set_instances_active_state(active_by_id) + + self._change_active_instances(active_by_id, new_value) group_names = set() - for instance_id in selected_ids: + for instance_id in active_by_id: group_name = self._group_by_instance_id.get(instance_id) if group_name is not None: group_names.add(group_name) @@ -917,16 +905,11 @@ class InstanceListView(AbstractInstanceView): if not instance_ids: return - changed_ids = set() for instance_id in instance_ids: widget = self._widgets_by_id.get(instance_id) if widget: - changed_ids.add(instance_id) widget.set_active(new_value) - if changed_ids: - self.active_changed.emit() - def _on_selection_change(self, *_args): self.selection_changed.emit() @@ -965,14 +948,16 @@ class InstanceListView(AbstractInstanceView): if not group_item: return - instance_ids = set() + active_by_id = {} for row in range(group_item.rowCount()): item = group_item.child(row) instance_id = item.data(INSTANCE_ID_ROLE) if instance_id is not None: - instance_ids.add(instance_id) + active_by_id[instance_id] = active - self._change_active_instances(instance_ids, active) + self._controller.set_instances_active_state(active_by_id) + + self._change_active_instances(active_by_id, active) proxy_index = self._proxy_model.mapFromSource(group_item.index()) if not self._instance_view.isExpanded(proxy_index): From 6a53253ca13b9e087861ae8d68824c56047d59ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:58:50 +0200 Subject: [PATCH 48/60] removed unused import --- client/ayon_core/tools/publisher/abstract.py | 1 - client/ayon_core/tools/publisher/models/create.py | 1 - 2 files changed, 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 4787e8a21b..a6ae93cecd 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -3,7 +3,6 @@ from typing import ( Optional, Dict, List, - Set, Tuple, Any, Callable, diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 2aa7b169a0..7cb46215df 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -4,7 +4,6 @@ from typing import ( Union, List, Dict, - Set, Tuple, Any, Optional, From f1a1e77134b5101d87715506d0ebc5591058b01f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:23:11 +0200 Subject: [PATCH 49/60] added new function to calculate representation delivery data --- client/ayon_core/pipeline/delivery.py | 80 ++++++++++++++++++++++- client/ayon_core/plugins/load/delivery.py | 33 ++++++---- 2 files changed, 100 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 029775e1db..174e81c194 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -3,11 +3,21 @@ import os import copy import shutil import glob -import clique import collections +from typing import List, Dict, Any, Iterable + +import clique +import ayon_api from ayon_core.lib import create_hard_link +from .anatomy import Anatomy +from .template_data import ( + get_general_template_data, + get_folder_template_data, + get_task_template_data, +) + def _copy_file(src_path, dst_path): """Hardlink file if possible(to save space), copy if not. @@ -327,3 +337,71 @@ def deliver_sequence( uploaded += 1 return report_items, uploaded + + +def _merge_data(data, new_data): + queue = collections.deque() + queue.append((data, new_data)) + while queue: + q_data, q_new_data = queue.popleft() + for key, value in q_new_data.items(): + if key in q_data and isinstance(value, dict): + queue.append((q_data[key], value)) + continue + q_data[key] = value + + +def get_representations_delivery_template_data( + project_name: str, + representation_ids: Iterable[str], +) -> Dict[str, Dict[str, Any]]: + representation_ids = set(representation_ids) + + output = { + repre_id: {} + for repre_id in representation_ids + } + if not representation_ids: + return output + + project_entity = ayon_api.get_project(project_name) + + general_template_data = get_general_template_data() + + repres_hierarchy = ayon_api.get_representations_hierarchy( + project_name, + representation_ids, + project_fields=set(), + folder_fields={"path", "folderType"}, + task_fields={"name", "taskType"}, + product_fields={"name", "productType"}, + version_fields={"version", "productId"}, + representation_fields=None, + ) + for repre_id, repre_hierarchy in repres_hierarchy.items(): + repre_entity = repre_hierarchy.representation + if repre_entity is None: + continue + + 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 + )) + if repre_hierarchy.task: + template_data.update(get_task_template_data( + project_entity, repre_hierarchy.task + )) + + product_entity = repre_hierarchy.product + version_entity = repre_hierarchy.version + template_data.update({ + "product": { + "name": product_entity["name"], + "type": product_entity["productType"], + }, + "version": version_entity["version"], + }) + _merge_data(template_data, repre_entity["context"]) + output[repre_id] = template_data + return output diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 5c53d170eb..3c9f1b9691 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -200,20 +200,29 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): format_dict = get_format_dict(self.anatomy, self.root_line_edit.text()) renumber_frame = self.renumber_frame.isChecked() frame_offset = self.first_frame_start.value() + filtered_repres = [] + repre_ids = set() for repre in self._representations: - if repre["name"] not in selected_repres: - continue + if repre["name"] in selected_repres: + filtered_repres.append(repre) + repre_ids.add(repre["id"]) + template_data_by_repre_id = get_representations_template_data( + self.anatomy.project_name, repre_ids + ) + for repre in filtered_repres: repre_path = get_representation_path_with_anatomy( repre, self.anatomy ) - anatomy_data = copy.deepcopy(repre["context"]) - new_report_items = check_destination_path(repre["id"], - self.anatomy, - anatomy_data, - datetime_data, - template_name) + template_data = template_data_by_repre_id[repre["id"]] + new_report_items = check_destination_path( + repre["id"], + self.anatomy, + template_data, + datetime_data, + template_name + ) report_items.update(new_report_items) if new_report_items: @@ -224,7 +233,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): repre, self.anatomy, template_name, - anatomy_data, + template_data, format_dict, report_items, self.log @@ -267,9 +276,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if frame is not None: if repre["context"].get("frame"): - anatomy_data["frame"] = frame + template_data["frame"] = frame elif repre["context"].get("udim"): - anatomy_data["udim"] = frame + template_data["udim"] = frame else: # Fallback self.log.warning( @@ -277,7 +286,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): " data. Supplying sequence frame to '{frame}'" " formatting data." ) - anatomy_data["frame"] = frame + template_data["frame"] = frame new_report_items, uploaded = deliver_single_file(*args) report_items.update(new_report_items) self._update_progress(uploaded) From ebdd757c669c6d72421abdb3df59742ad3eb4a03 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:55:24 +0200 Subject: [PATCH 50/60] fix imports --- client/ayon_core/pipeline/delivery.py | 3 +-- client/ayon_core/plugins/load/delivery.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 174e81c194..2a2adf984a 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -4,14 +4,13 @@ import copy import shutil import glob import collections -from typing import List, Dict, Any, Iterable +from typing import Dict, Any, Iterable import clique import ayon_api from ayon_core.lib import create_hard_link -from .anatomy import Anatomy from .template_data import ( get_general_template_data, get_folder_template_data, diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 3c9f1b9691..e1cd136b26 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -1,23 +1,22 @@ -import copy import platform from collections import defaultdict import ayon_api from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.pipeline import load, Anatomy from ayon_core import resources, style - from ayon_core.lib import ( format_file_size, collect_frames, get_datetime_data, ) +from ayon_core.pipeline import load, Anatomy from ayon_core.pipeline.load import get_representation_path_with_anatomy from ayon_core.pipeline.delivery import ( get_format_dict, check_destination_path, - deliver_single_file + deliver_single_file, + get_representations_template_data, ) From e175121d0a30ccef65fcc1478c05740badc1dd99 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:55:38 +0200 Subject: [PATCH 51/60] fix typo --- client/ayon_core/plugins/load/delivery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index e1cd136b26..559950c997 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -350,8 +350,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): def _get_selected_repres(self): """Returns list of representation names filtered from checkboxes.""" selected_repres = [] - for repre_name, chckbox in self._representation_checkboxes.items(): - if chckbox.isChecked(): + for repre_name, checkbox in self._representation_checkboxes.items(): + if checkbox.isChecked(): selected_repres.append(repre_name) return selected_repres From d15740e33f0d3d652ff4c9bed1b82d852d316acc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:30:50 +0200 Subject: [PATCH 52/60] fix import --- client/ayon_core/plugins/load/delivery.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 559950c997..406040d936 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -16,7 +16,7 @@ from ayon_core.pipeline.delivery import ( get_format_dict, check_destination_path, deliver_single_file, - get_representations_template_data, + get_representations_delivery_template_data, ) @@ -206,8 +206,10 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): filtered_repres.append(repre) repre_ids.add(repre["id"]) - template_data_by_repre_id = get_representations_template_data( - self.anatomy.project_name, repre_ids + template_data_by_repre_id = ( + get_representations_delivery_template_data( + self.anatomy.project_name, repre_ids + ) ) for repre in filtered_repres: repre_path = get_representation_path_with_anatomy( From ceedd7fbcd4489229aea76f6d64e89f76d233ea3 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 15 Oct 2024 14:22:34 +0000 Subject: [PATCH 53/60] [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 458129f367..e9ce613942 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.1+dev" +__version__ = "1.0.2" diff --git a/package.py b/package.py index c059eed423..a9c66833cc 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.1+dev" +version = "1.0.2" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 0a7d0d76c9..293be52d6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.1+dev" +version = "1.0.2" description = "" authors = ["Ynput Team "] readme = "README.md" From 50cad97cad63474ff5a51d2fc2214cfb45d2766c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 15 Oct 2024 14:23:14 +0000 Subject: [PATCH 54/60] [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 e9ce613942..8fa97eb9cb 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.2" +__version__ = "1.0.2+dev" diff --git a/package.py b/package.py index a9c66833cc..b5cd0a1903 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.2" +version = "1.0.2+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 293be52d6e..8b03020f6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.2" +version = "1.0.2+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From b239cdd8916e07c51bbed7ce7d024a911b554a4b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 16 Oct 2024 00:06:40 +0200 Subject: [PATCH 55/60] Fix single frame publishing (from e.g. Maya) Also fixes it for other hosts that use instance.data[`expectedFiles`] with the value being `list[dict[str, list[str]]]` (Basically the files per AOV, where the list of filenames is `list[str]` but the integrator and other areas really want a single `str` insteaf of `list[str]` if it's a single frame) --- client/ayon_core/pipeline/farm/pyblish_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index af90903bd8..98951b2766 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -788,15 +788,15 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, colorspace = product.colorspace break - if isinstance(files, (list, tuple)): - files = [os.path.basename(f) for f in files] + if isinstance(collected_files, (list, tuple)): + collected_files = [os.path.basename(f) for f in collected_files] else: - files = os.path.basename(files) + collected_files = os.path.basename(collected_files) rep = { "name": ext, "ext": ext, - "files": files, + "files": collected_files, "frameStart": int(skeleton["frameStartHandle"]), "frameEnd": int(skeleton["frameEndHandle"]), # If expectedFile are absolute, we need only filenames From 1eb25ef945ffe0fe279f0ad4e4e54ba818f85e00 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 16 Oct 2024 09:34:12 +0000 Subject: [PATCH 56/60] [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 8fa97eb9cb..3da4af0b4e 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.2+dev" +__version__ = "1.0.3" diff --git a/package.py b/package.py index b5cd0a1903..a5319dd139 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.2+dev" +version = "1.0.3" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 8b03020f6d..9b2b13ffa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.2+dev" +version = "1.0.3" description = "" authors = ["Ynput Team "] readme = "README.md" From 47b5d90495563be2660d8b10b1b0479c3d1330ad Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 16 Oct 2024 09:34:49 +0000 Subject: [PATCH 57/60] [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 3da4af0b4e..9a951d7fd4 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.3" +__version__ = "1.0.3+dev" diff --git a/package.py b/package.py index a5319dd139..5d5218748c 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.3" +version = "1.0.3+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 9b2b13ffa8..ebf08be4a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.3" +version = "1.0.3+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From c93f3449b6b419eb429b641a65449e7bc44c68ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:15:49 +0200 Subject: [PATCH 58/60] ignore publish attributes without attribute definitions --- client/ayon_core/tools/publisher/models/create.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 7cb46215df..9c13d8ae2f 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -27,6 +27,7 @@ from ayon_core.pipeline.create import ( Creator, CreateContext, CreatedInstance, + AttributeValues, ) from ayon_core.pipeline.create import ( CreatorsOperationFailed, @@ -857,7 +858,10 @@ class CreateModel: item_id = None if isinstance(item, CreatedInstance): item_id = item.id + for plugin_name, attr_val in item.publish_attributes.items(): + if not isinstance(attr_val, AttributeValues): + continue attr_defs = attr_val.attr_defs if not attr_defs: continue From 0e8b129d6af008397e48a850544b18b75065722b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 23 Oct 2024 14:49:16 +0000 Subject: [PATCH 59/60] [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 9a951d7fd4..47da5b3a1b 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.3+dev" +__version__ = "1.0.4" diff --git a/package.py b/package.py index 5d5218748c..0ba9303182 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.3+dev" +version = "1.0.4" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index ebf08be4a8..64b389ea3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.3+dev" +version = "1.0.4" description = "" authors = ["Ynput Team "] readme = "README.md" From d2ee4167ae0151908eda349d582663bf193efdd9 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 23 Oct 2024 14:49:58 +0000 Subject: [PATCH 60/60] [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 47da5b3a1b..8a7065c93c 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" +__version__ = "1.0.4+dev" diff --git a/package.py b/package.py index 0ba9303182..7c5bffe81f 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.4" +version = "1.0.4+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 64b389ea3e..c686d685fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.4" +version = "1.0.4+dev" description = "" authors = ["Ynput Team "] readme = "README.md"