Skip to content

Commit

Permalink
Merge branch 'develop' into enhancement/AY-2420_Callbacks-and-groups-…
Browse files Browse the repository at this point in the history
…with-Publisher-attributes
  • Loading branch information
iLLiCiTiT authored Oct 9, 2024
2 parents 8732930 + d759c9b commit d51b568
Show file tree
Hide file tree
Showing 2 changed files with 293 additions and 2 deletions.
273 changes: 273 additions & 0 deletions client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
"""
Brought from https://gist.github.com/BigRoy/1972822065e38f8fae7521078e44eca2
Code Credits: [BigRoy](https://github.com/BigRoy)
Requirement:
It requires pyblish version >= 1.8.12
How it works:
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 :
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
"""

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)) # noqa
value_item = self.itemFromIndex(self.index(row, 2, parent_index)) # noqa
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 # 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
):
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 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(QtGui.QFont.TypeWriter)
text_edit.setFont(font)
text_edit.setLineWrapMode(QtWidgets.QTextEdit.NoWrap)

step = QtWidgets.QPushButton("Step")
step.setEnabled(False)

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)

step.clicked.connect(self.on_step)

self._pause = False
self.model = model
self.filter = filter_field
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("pluginProcessed",
self.on_plugin_processed)

def hideEvent(self, event):
self.pause(False)
print("Deregistering callback..")
pyblish.api.deregister_callback("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}<br>"
msg += f"Plugin: {plugin_name}"
if plugin_instance is not None:
msg += f" -> instance: {plugin_instance}"
msg += "<br>"
msg += f"Duration: {duration} ms<br>"
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()
22 changes: 20 additions & 2 deletions client/ayon_core/tools/experimental_tools/tools_def.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -95,6 +96,12 @@ def __init__(self, parent_widget=None, refresh=True):
"hiero",
"resolve",
]
),
ExperimentalHostTool(
"pyblish_debug_stepper",
"Pyblish Debug Stepper",
"Debug Pyblish plugins step by step.",
self._show_pyblish_debugger,
)
]

Expand Down Expand Up @@ -162,9 +169,16 @@ def refresh_availability(self):
local_settings.get(LOCAL_EXPERIMENTAL_KEY)
) or {}

for identifier, eperimental_tool in self.tools_by_identifier.items():
# Enable the following tools by default.
# Because they will always be disabled due
# to the fact their settings don't exist.
experimental_settings.update({
"pyblish_debug_stepper": True,
})

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:
Expand All @@ -175,3 +189,7 @@ def _show_publisher(self):
)

self._publisher_tool.show()

def _show_pyblish_debugger(self):
window = DebugUI(parent=self._parent_widget)
window.show()

0 comments on commit d51b568

Please sign in to comment.