From 8088534caa55bacecdf0edd5c9825cd9b80b1908 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 14:49:55 +0100 Subject: [PATCH 01/29] implemented 'create_hard_link' function in openpype lib --- openpype/lib/__init__.py | 2 ++ openpype/lib/path_tools.py | 1 - openpype/lib/vendor_bin_utils.py | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 6a24f30455..e1006303db 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -58,6 +58,7 @@ from .anatomy import ( from .config import get_datetime_data from .vendor_bin_utils import ( + create_hard_link, get_vendor_bin_path, get_oiio_tools_path, get_ffmpeg_tool_path, @@ -208,6 +209,7 @@ __all__ = [ "get_paths_from_environ", "get_global_environments", + "create_hard_link", "get_vendor_bin_path", "get_oiio_tools_path", "get_ffmpeg_tool_path", diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index d6c32ad9e8..71fc0fe25c 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -6,7 +6,6 @@ import logging import six from openpype.settings import get_project_settings -from openpype.settings.lib import get_site_local_overrides from .anatomy import Anatomy from .profiles_filtering import filter_profiles diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 4c2cf93dfa..fcc15a31f0 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -8,6 +8,41 @@ import distutils log = logging.getLogger("FFmpeg utils") +def create_hard_link(src_path, dst_path): + """Create hardlink of file. + + Args: + src_path(str): Full path to a file which is used as source for + hardlink. + dst_path(str): Full path to a file where a link of source will be + added. + """ + # Use `os.link` if is available + # - should be for all platforms with newer python versions + if hasattr(os, "link"): + os.link(src_path, dst_path) + return + + # Windows implementation of hardlinks + # - used in Python 2 + if platform.system().lower() == "windows": + import ctypes + from ctypes.wintypes import BOOL + CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW + CreateHardLink.argtypes = [ + ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p + ] + CreateHardLink.restype = BOOL + + res = CreateHardLink(dst_path, src_path, None) + if res == 0: + raise ctypes.WinError() + # Raises not implemented error if gets here + raise NotImplementedError( + "Implementation of hardlink for current environment is missing." + ) + + def get_vendor_bin_path(bin_app): """Path to OpenPype vendorized binaries. From c57fc0391677b0b48a06ba4c723936cf46071c5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 14:50:14 +0100 Subject: [PATCH 02/29] use create_hard_link instead of filelink --- openpype/lib/delivery.py | 7 +++---- openpype/plugins/publish/integrate_hero_version.py | 4 ++-- openpype/plugins/publish/integrate_new.py | 8 +++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index a61603fa05..9fc65aae8e 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -71,15 +71,14 @@ def path_from_representation(representation, anatomy): def copy_file(src_path, dst_path): """Hardlink file if possible(to save space), copy if not""" - from avalon.vendor import filelink # safer importing + from openpype.lib import create_hard_link # safer importing if os.path.exists(dst_path): return try: - filelink.create( + create_hard_link( src_path, - dst_path, - filelink.HARDLINK + dst_path ) except OSError: shutil.copyfile(src_path, dst_path) diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index ec836954e8..60245314f4 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -7,7 +7,7 @@ import shutil from pymongo import InsertOne, ReplaceOne import pyblish.api from avalon import api, io, schema -from avalon.vendor import filelink +from openpype.lib import create_hard_link class IntegrateHeroVersion(pyblish.api.InstancePlugin): @@ -518,7 +518,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): # First try hardlink and copy if paths are cross drive try: - filelink.create(src_path, dst_path, filelink.HARDLINK) + create_hard_link(src_path, dst_path) # Return when successful return diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6e0940d459..f9ab46b6fd 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -13,12 +13,14 @@ from pymongo import DeleteOne, InsertOne import pyblish.api from avalon import io from avalon.api import format_template_with_optional_keys -from avalon.vendor import filelink import openpype.api from datetime import datetime # from pype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles -from openpype.lib import prepare_template_data +from openpype.lib import ( + prepare_template_data, + create_hard_link +) # this is needed until speedcopy for linux is fixed if sys.platform == "win32": @@ -730,7 +732,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.critical("An unexpected error occurred.") six.reraise(*sys.exc_info()) - filelink.create(src, dst, filelink.HARDLINK) + create_hard_link(src, dst) def get_subset(self, asset, instance): subset_name = instance.data["subset"] From e48af4585734f2354ea0133d7fafc9c0922aefca Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 17:22:51 +0100 Subject: [PATCH 03/29] moved qargparse into openpype vendor --- openpype/hosts/flame/api/plugin.py | 2 +- openpype/hosts/hiero/api/plugin.py | 2 +- openpype/hosts/maya/api/plugin.py | 3 +- openpype/hosts/nuke/plugins/load/load_clip.py | 2 +- .../hosts/nuke/plugins/load/load_image.py | 3 +- .../plugins/load/load_image_from_sequence.py | 3 +- openpype/hosts/resolve/api/plugin.py | 6 +- .../hosts/tvpaint/plugins/load/load_image.py | 2 +- .../plugins/load/load_reference_image.py | 2 +- openpype/plugins/load/delete_old_versions.py | 2 +- openpype/tools/utils/widgets.py | 4 +- openpype/vendor/python/common/qargparse.py | 817 ++++++++++++++++++ 12 files changed, 833 insertions(+), 15 deletions(-) create mode 100644 openpype/vendor/python/common/qargparse.py diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index ec49db1601..0850faf98d 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -2,9 +2,9 @@ import os import re import shutil import sys -from avalon.vendor import qargparse from xml.etree import ElementTree as ET import six +import qargparse from Qt import QtWidgets, QtCore import openpype.api as openpype from openpype import style diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 3506af2d6a..3d7bdeab68 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -2,7 +2,7 @@ import re import os import hiero from Qt import QtWidgets, QtCore -from avalon.vendor import qargparse +import qargparse import avalon.api as avalon import openpype.api as openpype from . import lib diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index bdb8fcf13a..547b125eb4 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -2,8 +2,9 @@ import os from maya import cmds +import qargparse + from avalon import api -from avalon.vendor import qargparse from openpype.api import PypeCreatorMixin from .pipeline import containerise diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 21b7a6a816..a253ba4a9d 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -1,5 +1,5 @@ import nuke -from avalon.vendor import qargparse +import qargparse from avalon import api, io from openpype.hosts.nuke.api.lib import ( diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index d36226b139..27c634ec57 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -1,7 +1,6 @@ -import re import nuke -from avalon.vendor import qargparse +import qargparse from avalon import api, io from openpype.hosts.nuke.api.lib import ( diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 6627aded51..12e0503dfc 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -1,7 +1,7 @@ import os +import qargparse from avalon.pipeline import get_representation_path_from_context -from avalon.vendor import qargparse from openpype.hosts.photoshop import api as photoshop from openpype.hosts.photoshop.api import get_unique_layer_name @@ -92,4 +92,3 @@ class ImageFromSequenceLoader(photoshop.PhotoshopLoader): def remove(self, container): """No update possible, not containerized.""" pass - diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 8612cf82ec..3f4476e18e 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -1,12 +1,14 @@ import re import uuid + +import qargparse +from Qt import QtWidgets, QtCore + from avalon import api import openpype.api as pype from openpype.hosts import resolve -from avalon.vendor import qargparse from . import lib -from Qt import QtWidgets, QtCore class CreatorWidget(QtWidgets.QDialog): diff --git a/openpype/hosts/tvpaint/plugins/load/load_image.py b/openpype/hosts/tvpaint/plugins/load/load_image.py index 7dba1e3619..f861d0119e 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_image.py @@ -1,4 +1,4 @@ -from avalon.vendor import qargparse +import qargparse from openpype.hosts.tvpaint.api import lib, plugin diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index 0a85e5dc76..5e4e3965d2 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -1,6 +1,6 @@ import collections +import qargparse from avalon.pipeline import get_representation_context -from avalon.vendor import qargparse from openpype.hosts.tvpaint.api import lib, pipeline, plugin diff --git a/openpype/plugins/load/delete_old_versions.py b/openpype/plugins/load/delete_old_versions.py index b2f2c88975..e8612745fb 100644 --- a/openpype/plugins/load/delete_old_versions.py +++ b/openpype/plugins/load/delete_old_versions.py @@ -5,10 +5,10 @@ import uuid import clique from pymongo import UpdateOne import ftrack_api +import qargparse from Qt import QtWidgets, QtCore from avalon import api, style -from avalon.vendor import qargparse from avalon.api import AvalonMongoDB import avalon.pipeline from openpype.api import Anatomy diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index a4e172ea5c..783736a9ca 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -1,8 +1,8 @@ import logging from Qt import QtWidgets, QtCore, QtGui - -from avalon.vendor import qtawesome, qargparse +import qargparse +from avalon.vendor import qtawesome from openpype.style import ( get_objected_colors, get_style_image_path diff --git a/openpype/vendor/python/common/qargparse.py b/openpype/vendor/python/common/qargparse.py new file mode 100644 index 0000000000..ebde9ae76d --- /dev/null +++ b/openpype/vendor/python/common/qargparse.py @@ -0,0 +1,817 @@ +""" +NOTE: The required `Qt` module has changed to use the one that vendorized. + Remember to change to relative import when updating this. +""" + +import re +import logging + +from collections import OrderedDict as odict +from Qt import QtCore, QtWidgets, QtGui +import qtawesome + +__version__ = "0.5.2" +_log = logging.getLogger(__name__) +_type = type # used as argument + +try: + # Python 2 + _basestring = basestring +except NameError: + _basestring = str + + +class QArgumentParser(QtWidgets.QWidget): + """User interface arguments + + Arguments: + arguments (list, optional): Instances of QArgument + description (str, optional): Long-form text of what this parser is for + storage (QSettings, optional): Persistence to disk, providing + value() and setValue() methods + + """ + + changed = QtCore.Signal(QtCore.QObject) # A QArgument + + def __init__(self, + arguments=None, + description=None, + storage=None, + parent=None): + super(QArgumentParser, self).__init__(parent) + self.setAttribute(QtCore.Qt.WA_StyledBackground) + + # Create internal settings + if storage is True: + storage = QtCore.QSettings( + QtCore.QSettings.IniFormat, + QtCore.QSettings.UserScope, + __name__, "QArgparse", + ) + + if storage is not None: + _log.info("Storing settings @ %s" % storage.fileName()) + + arguments = arguments or [] + + assert hasattr(arguments, "__iter__"), "arguments must be iterable" + assert isinstance(storage, (type(None), QtCore.QSettings)), ( + "storage must be of type QSettings" + ) + + layout = QtWidgets.QGridLayout(self) + layout.setRowStretch(999, 1) + + if description: + layout.addWidget(QtWidgets.QLabel(description), 0, 0, 1, 2) + + self._row = 1 + self._storage = storage + self._arguments = odict() + self._desciption = description + + for arg in arguments or []: + self._addArgument(arg) + + self.setStyleSheet(style) + + def setDescription(self, text): + self._desciption.setText(text) + + def addArgument(self, name, type=None, default=None, **kwargs): + # Infer type from default + if type is None and default is not None: + type = _type(default) + + # Default to string + type = type or str + + Argument = { + None: String, + int: Integer, + float: Float, + bool: Boolean, + str: String, + list: Enum, + tuple: Enum, + }.get(type, type) + + arg = Argument(name, default=default, **kwargs) + self._addArgument(arg) + return arg + + def _addArgument(self, arg): + if arg["name"] in self._arguments: + raise ValueError("Duplicate argument '%s'" % arg["name"]) + + if self._storage is not None: + default = self._storage.value(arg["name"]) + + if default: + if isinstance(arg, Boolean): + default = bool({ + None: QtCore.Qt.Unchecked, + + 0: QtCore.Qt.Unchecked, + 1: QtCore.Qt.Checked, + 2: QtCore.Qt.Checked, + + "0": QtCore.Qt.Unchecked, + "1": QtCore.Qt.Checked, + "2": QtCore.Qt.Checked, + + # May be stored as string, if used with IniFormat + "false": QtCore.Qt.Unchecked, + "true": QtCore.Qt.Checked, + }.get(default)) + + arg["default"] = default + + arg.changed.connect(lambda: self.on_changed(arg)) + + label = ( + QtWidgets.QLabel(arg["label"]) + if arg.label + else QtWidgets.QLabel() + ) + widget = arg.create() + icon = qtawesome.icon("fa.refresh", color="white") + reset = QtWidgets.QPushButton(icon, "") # default + reset.setToolTip("Reset") + reset.setProperty("type", "reset") + reset.clicked.connect(lambda: self.on_reset(arg)) + + # Shown on edit + reset.hide() + + for widget in (label, widget): + widget.setToolTip(arg["help"]) + widget.setObjectName(arg["name"]) # useful in CSS + widget.setProperty("type", type(arg).__name__) + widget.setAttribute(QtCore.Qt.WA_StyledBackground) + widget.setEnabled(arg["enabled"]) + + # Align label on top of row if widget is over two times heiger + height = (lambda w: w.sizeHint().height()) + label_on_top = height(label) * 2 < height(widget) + alignment = (QtCore.Qt.AlignTop,) if label_on_top else () + + layout = self.layout() + layout.addWidget(label, self._row, 0, *alignment) + layout.addWidget(widget, self._row, 1) + layout.addWidget(reset, self._row, 2, *alignment) + layout.setColumnStretch(1, 1) + + def on_changed(*_): + reset.setVisible(arg["edited"]) + + arg.changed.connect(on_changed) + + self._row += 1 + self._arguments[arg["name"]] = arg + + def clear(self): + assert self._storage, "Cannot clear without persistent storage" + self._storage.clear() + _log.info("Clearing settings @ %s" % self._storage.fileName()) + + def find(self, name): + return self._arguments[name] + + def on_reset(self, arg): + arg.write(arg["default"]) + + def on_changed(self, arg): + arg["edited"] = arg.read() != arg["default"] + self.changed.emit(arg) + + # Optional PEP08 syntax + add_argument = addArgument + + +class QArgument(QtCore.QObject): + """Base class of argument user interface + """ + changed = QtCore.Signal() + + # Provide a left-hand side label for this argument + label = True + # For defining default value for each argument type + default = None + + def __init__(self, name, default=None, **kwargs): + super(QArgument, self).__init__(kwargs.pop("parent", None)) + + kwargs["name"] = name + kwargs["label"] = kwargs.get("label", camel_to_title(name)) + kwargs["default"] = self.default if default is None else default + kwargs["help"] = kwargs.get("help", "") + kwargs["read"] = kwargs.get("read") + kwargs["write"] = kwargs.get("write") + kwargs["enabled"] = bool(kwargs.get("enabled", True)) + kwargs["edited"] = False + + self._data = kwargs + + def __str__(self): + return self["name"] + + def __repr__(self): + return "%s(\"%s\")" % (type(self).__name__, self["name"]) + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, value): + self._data[key] = value + + def __eq__(self, other): + if isinstance(other, _basestring): + return self["name"] == other + return super(QArgument, self).__eq__(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def create(self): + return QtWidgets.QWidget() + + def read(self): + return self._read() + + def write(self, value): + self._write(value) + self.changed.emit() + + +class Boolean(QArgument): + """Boolean type user interface + + Presented by `QtWidgets.QCheckBox`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (bool, optional): Argument's default value, default None + enabled (bool, optional): Whether to enable this widget, default True + + """ + def create(self): + widget = QtWidgets.QCheckBox() + widget.clicked.connect(self.changed.emit) + + if isinstance(self, Tristate): + self._read = lambda: widget.checkState() + state = { + 0: QtCore.Qt.Unchecked, + 1: QtCore.Qt.PartiallyChecked, + 2: QtCore.Qt.Checked, + "1": QtCore.Qt.PartiallyChecked, + "0": QtCore.Qt.Unchecked, + "2": QtCore.Qt.Checked, + } + else: + self._read = lambda: bool(widget.checkState()) + state = { + None: QtCore.Qt.Unchecked, + + 0: QtCore.Qt.Unchecked, + 1: QtCore.Qt.Checked, + 2: QtCore.Qt.Checked, + + "0": QtCore.Qt.Unchecked, + "1": QtCore.Qt.Checked, + "2": QtCore.Qt.Checked, + + # May be stored as string, if used with QSettings(..IniFormat) + "false": QtCore.Qt.Unchecked, + "true": QtCore.Qt.Checked, + } + + self._write = lambda value: widget.setCheckState(state[value]) + widget.clicked.connect(self.changed.emit) + + if self["default"] is not None: + self._write(self["default"]) + + return widget + + def read(self): + return self._read() + + +class Tristate(QArgument): + """Not implemented""" + + +class Number(QArgument): + """Base class of numeric type user interface""" + default = 0 + + def create(self): + if isinstance(self, Float): + widget = QtWidgets.QDoubleSpinBox() + widget.setMinimum(self._data.get("min", 0.0)) + widget.setMaximum(self._data.get("max", 99.99)) + else: + widget = QtWidgets.QSpinBox() + widget.setMinimum(self._data.get("min", 0)) + widget.setMaximum(self._data.get("max", 99)) + + widget.editingFinished.connect(self.changed.emit) + self._read = lambda: widget.value() + self._write = lambda value: widget.setValue(value) + + if self["default"] != self.default: + self._write(self["default"]) + + return widget + + +class Integer(Number): + """Integer type user interface + + A subclass of `qargparse.Number`, presented by `QtWidgets.QSpinBox`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (int, optional): Argument's default value, default 0 + min (int, optional): Argument's minimum value, default 0 + max (int, optional): Argument's maximum value, default 99 + enabled (bool, optional): Whether to enable this widget, default True + + """ + + +class Float(Number): + """Float type user interface + + A subclass of `qargparse.Number`, presented by `QtWidgets.QDoubleSpinBox`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (float, optional): Argument's default value, default 0.0 + min (float, optional): Argument's minimum value, default 0.0 + max (float, optional): Argument's maximum value, default 99.99 + enabled (bool, optional): Whether to enable this widget, default True + + """ + + +class Range(Number): + """Range type user interface + + A subclass of `qargparse.Number`, not production ready. + + """ + + +class Double3(QArgument): + """Double3 type user interface + + Presented by three `QtWidgets.QLineEdit` widget with `QDoubleValidator` + installed. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (tuple or list, optional): Default (0, 0, 0). + enabled (bool, optional): Whether to enable this widget, default True + + """ + default = (0, 0, 0) + + def create(self): + widget = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(widget) + layout.setContentsMargins(0, 0, 0, 0) + x, y, z = (self.child_arg(layout, i) for i in range(3)) + + self._read = lambda: ( + float(x.text()), float(y.text()), float(z.text())) + self._write = lambda value: [ + w.setText(str(float(v))) for w, v in zip([x, y, z], value)] + + if self["default"] != self.default: + self._write(self["default"]) + + return widget + + def child_arg(self, layout, index): + widget = QtWidgets.QLineEdit() + widget.setValidator(QtGui.QDoubleValidator()) + + default = str(float(self["default"][index])) + widget.setText(default) + + def focusOutEvent(event): + if not widget.text(): + widget.setText(default) # Ensure value exists for `_read` + QtWidgets.QLineEdit.focusOutEvent(widget, event) + widget.focusOutEvent = focusOutEvent + + widget.editingFinished.connect(self.changed.emit) + widget.returnPressed.connect(widget.editingFinished.emit) + + layout.addWidget(widget) + + return widget + + +class String(QArgument): + """String type user interface + + Presented by `QtWidgets.QLineEdit`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (str, optional): Argument's default value, default None + placeholder (str, optional): Placeholder message for the widget + enabled (bool, optional): Whether to enable this widget, default True + + """ + def __init__(self, *args, **kwargs): + super(String, self).__init__(*args, **kwargs) + self._previous = None + + def create(self): + widget = QtWidgets.QLineEdit() + widget.editingFinished.connect(self.onEditingFinished) + widget.returnPressed.connect(widget.editingFinished.emit) + self._read = lambda: widget.text() + self._write = lambda value: widget.setText(value) + + if isinstance(self, Info): + widget.setReadOnly(True) + widget.setPlaceholderText(self._data.get("placeholder", "")) + + if self["default"] is not None: + self._write(self["default"]) + self._previous = self["default"] + + return widget + + def onEditingFinished(self): + current = self._read() + + if current != self._previous: + self.changed.emit() + self._previous = current + + +class Info(String): + """String type user interface but read-only + + A subclass of `qargparse.String`, presented by `QtWidgets.QLineEdit`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (str, optional): Argument's default value, default None + enabled (bool, optional): Whether to enable this widget, default True + + """ + + +class Color(String): + """Color type user interface + + A subclass of `qargparse.String`, not production ready. + + """ + + +class Button(QArgument): + """Button type user interface + + Presented by `QtWidgets.QPushButton`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (bool, optional): Argument's default value, default None + enabled (bool, optional): Whether to enable this widget, default True + + """ + label = False + + def create(self): + widget = QtWidgets.QPushButton(self["label"]) + widget.clicked.connect(self.changed.emit) + + state = [ + QtCore.Qt.Unchecked, + QtCore.Qt.Checked, + ] + + if isinstance(self, Toggle): + widget.setCheckable(True) + if hasattr(widget, "isChecked"): + self._read = lambda: state[int(widget.isChecked())] + self._write = ( + lambda value: widget.setChecked(value) + ) + else: + self._read = lambda: widget.checkState() + self._write = ( + lambda value: widget.setCheckState(state[int(value)]) + ) + else: + self._read = lambda: "clicked" + self._write = lambda value: None + + if self["default"] is not None: + self._write(self["default"]) + + return widget + + +class Toggle(Button): + """Checkable `Button` type user interface + + Presented by `QtWidgets.QPushButton`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (bool, optional): Argument's default value, default None + enabled (bool, optional): Whether to enable this widget, default True + + """ + + +class InfoList(QArgument): + """String list type user interface + + Presented by `QtWidgets.QListView`, not production ready. + + """ + def __init__(self, name, **kwargs): + kwargs["default"] = kwargs.pop("default", ["Empty"]) + super(InfoList, self).__init__(name, **kwargs) + + def create(self): + class Model(QtCore.QStringListModel): + def data(self, index, role): + return super(Model, self).data(index, role) + + model = QtCore.QStringListModel(self["default"]) + widget = QtWidgets.QListView() + widget.setModel(model) + widget.setEditTriggers(widget.NoEditTriggers) + + self._read = lambda: model.stringList() + self._write = lambda value: model.setStringList(value) + + return widget + + +class Choice(QArgument): + """Argument user interface for selecting one from list + + Presented by `QtWidgets.QListView`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + items (list, optional): List of strings for select, default `["Empty"]` + default (str, optional): Default item in `items`, use first of `items` + if not given. + enabled (bool, optional): Whether to enable this widget, default True + + """ + def __init__(self, name, **kwargs): + kwargs["items"] = kwargs.get("items", ["Empty"]) + kwargs["default"] = kwargs.pop("default", kwargs["items"][0]) + super(Choice, self).__init__(name, **kwargs) + + def index(self, value): + """Return numerical equivalent to self.read()""" + return self["items"].index(value) + + def create(self): + def on_changed(selected, deselected): + try: + selected = selected.indexes()[0] + except IndexError: + # At least one item must be selected at all times + selected = deselected.indexes()[0] + + value = selected.data(QtCore.Qt.DisplayRole) + set_current(value) + self.changed.emit() + + def set_current(current): + options = model.stringList() + + if current == "Empty": + index = 0 + else: + for index, member in enumerate(options): + if member == current: + break + else: + raise ValueError( + "%s not a member of %s" % (current, options) + ) + + qindex = model.index(index, 0, QtCore.QModelIndex()) + smodel.setCurrentIndex(qindex, smodel.ClearAndSelect) + self["current"] = options[index] + + def reset(items, default=None): + items = items or ["Empty"] + model.setStringList(items) + set_current(default or items[0]) + + model = QtCore.QStringListModel() + widget = QtWidgets.QListView() + widget.setModel(model) + widget.setEditTriggers(widget.NoEditTriggers) + widget.setSelectionMode(widget.SingleSelection) + smodel = widget.selectionModel() + smodel.selectionChanged.connect(on_changed) + + self._read = lambda: self["current"] + self._write = lambda value: set_current(value) + self.reset = reset + + reset(self["items"], self["default"]) + + return widget + + +class Separator(QArgument): + """Visual separator + + Example: + + item1 + item2 + ------------ + item3 + item4 + + """ + + def create(self): + widget = QtWidgets.QWidget() + + self._read = lambda: None + self._write = lambda value: None + + return widget + + +class Enum(QArgument): + """Argument user interface for selecting one from dropdown list + + Presented by `QtWidgets.QComboBox`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + items (list, optional): List of strings for select, default `[]` + default (int, optional): Index of default item, use first of `items` + if not given. + enabled (bool, optional): Whether to enable this widget, default True + + """ + def __init__(self, name, **kwargs): + kwargs["default"] = kwargs.pop("default", 0) + kwargs["items"] = kwargs.get("items", []) + + assert isinstance(kwargs["items"], (tuple, list)), ( + "items must be list" + ) + + super(Enum, self).__init__(name, **kwargs) + + def create(self): + widget = QtWidgets.QComboBox() + widget.addItems(self["items"]) + widget.currentIndexChanged.connect( + lambda index: self.changed.emit()) + + self._read = lambda: widget.currentText() + self._write = lambda value: widget.setCurrentIndex(value) + + if self["default"] is not None: + self._write(self["default"]) + + return widget + + +style = """\ +QWidget { + /* Explicitly specify a size, to account for automatic HDPi */ + font-size: 11px; +} + +*[type="Button"] { + text-align:left; +} + +*[type="Info"] { + background: transparent; + border: none; +} + +QLabel[type="Separator"] { + min-height: 20px; + text-decoration: underline; +} + +QPushButton[type="reset"] { + max-width: 11px; + max-height: 11px; +} + +""" + + +def camelToTitle(text): + """Convert camelCase `text` to Title Case + + Example: + >>> camelToTitle("mixedCase") + "Mixed Case" + >>> camelToTitle("myName") + "My Name" + >>> camelToTitle("you") + "You" + >>> camelToTitle("You") + "You" + >>> camelToTitle("This is That") + "This Is That" + + """ + + return re.sub( + r"((?<=[a-z])[A-Z]|(? Date: Mon, 7 Mar 2022 17:42:34 +0100 Subject: [PATCH 04/29] added qtawesome and qtpy into poetry lock --- .../hosts/fusion/scripts/set_rendermode.py | 2 +- .../hosts/fusion/utility_scripts/switch_ui.py | 2 +- openpype/tools/assetcreator/widget.py | 2 +- openpype/tools/creator/widgets.py | 3 +- openpype/tools/launcher/lib.py | 2 +- openpype/tools/launcher/models.py | 2 +- openpype/tools/launcher/widgets.py | 2 +- openpype/tools/launcher/window.py | 2 +- openpype/tools/loader/lib.py | 2 +- openpype/tools/loader/model.py | 2 +- openpype/tools/mayalookassigner/models.py | 2 +- .../project_manager/project_manager/style.py | 4 +-- openpype/tools/publisher/widgets/widgets.py | 3 +- openpype/tools/pyblish_pype/model.py | 3 +- openpype/tools/sceneinventory/model.py | 4 +-- .../tools/sceneinventory/switch_dialog.py | 2 +- openpype/tools/sceneinventory/view.py | 2 +- openpype/tools/sceneinventory/window.py | 2 +- .../tools/settings/settings/categories.py | 3 +- openpype/tools/settings/settings/widgets.py | 2 +- .../standalonepublish/widgets/model_asset.py | 2 +- .../widgets/model_tasks_template.py | 2 +- .../standalonepublish/widgets/widget_asset.py | 2 +- .../widgets/widget_family_desc.py | 6 ++-- openpype/tools/subsetmanager/window.py | 2 +- openpype/tools/utils/assets_widget.py | 2 +- openpype/tools/utils/lib.py | 2 +- openpype/tools/utils/tasks_widget.py | 3 +- openpype/tools/utils/widgets.py | 2 +- openpype/tools/workfiles/model.py | 2 +- poetry.lock | 28 +++++++++++++++++++ pyproject.toml | 2 ++ 32 files changed, 65 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/fusion/scripts/set_rendermode.py b/openpype/hosts/fusion/scripts/set_rendermode.py index 77a2d8e945..f0638e4fe3 100644 --- a/openpype/hosts/fusion/scripts/set_rendermode.py +++ b/openpype/hosts/fusion/scripts/set_rendermode.py @@ -1,5 +1,5 @@ from Qt import QtWidgets -from avalon.vendor import qtawesome +import qtawesome from openpype.hosts.fusion.api import get_current_comp diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/utility_scripts/switch_ui.py index afb39f7041..d9eeae25ea 100644 --- a/openpype/hosts/fusion/utility_scripts/switch_ui.py +++ b/openpype/hosts/fusion/utility_scripts/switch_ui.py @@ -6,7 +6,7 @@ from Qt import QtWidgets, QtCore import avalon.api from avalon import io -from avalon.vendor import qtawesome as qta +import qtawesome as qta from openpype import style from openpype.hosts.fusion import api diff --git a/openpype/tools/assetcreator/widget.py b/openpype/tools/assetcreator/widget.py index fd0f438e68..9ad7e19692 100644 --- a/openpype/tools/assetcreator/widget.py +++ b/openpype/tools/assetcreator/widget.py @@ -2,7 +2,7 @@ import logging import contextlib import collections -from avalon.vendor import qtawesome +import qtawesome from Qt import QtWidgets, QtCore, QtGui from avalon import style, io diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index 9dd435c1cc..43df08496b 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -3,9 +3,8 @@ import inspect from Qt import QtWidgets, QtCore, QtGui -from avalon.vendor import qtawesome +import qtawesome -from openpype import style from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from openpype.tools.utils import ErrorMessageBox diff --git a/openpype/tools/launcher/lib.py b/openpype/tools/launcher/lib.py index b4e6a0c3e9..68c759f295 100644 --- a/openpype/tools/launcher/lib.py +++ b/openpype/tools/launcher/lib.py @@ -16,7 +16,7 @@ provides a bridge between the file-based project inventory and configuration. import os from Qt import QtGui -from avalon.vendor import qtawesome +import qtawesome from openpype.api import resources ICON_CACHE = {} diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index effa283318..9036c9cbd5 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -7,7 +7,7 @@ import time import appdirs from Qt import QtCore, QtGui -from avalon.vendor import qtawesome +import qtawesome from avalon import api from openpype.lib import JSONSettingRegistry from openpype.lib.applications import ( diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 30e6531843..62599664fe 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -2,7 +2,7 @@ import copy import time import collections from Qt import QtWidgets, QtCore, QtGui -from avalon.vendor import qtawesome +import qtawesome from openpype.tools.flickcharm import FlickCharm from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index b5b6368865..d80b3eabf0 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -8,7 +8,7 @@ from avalon.api import AvalonMongoDB from openpype import style from openpype.api import resources -from avalon.vendor import qtawesome +import qtawesome from .models import ( LauncherModel, ProjectModel diff --git a/openpype/tools/loader/lib.py b/openpype/tools/loader/lib.py index 180dee3eb5..28e94237ec 100644 --- a/openpype/tools/loader/lib.py +++ b/openpype/tools/loader/lib.py @@ -1,7 +1,7 @@ import inspect from Qt import QtGui +import qtawesome -from avalon.vendor import qtawesome from openpype.tools.utils.widgets import ( OptionalAction, OptionDialog diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 10b22d0e17..baee569239 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -8,8 +8,8 @@ from avalon import ( schema ) from Qt import QtCore, QtGui +import qtawesome -from avalon.vendor import qtawesome from avalon.lib import HeroVersionType from openpype.tools.utils.models import TreeModel, Item diff --git a/openpype/tools/mayalookassigner/models.py b/openpype/tools/mayalookassigner/models.py index 39cab83c61..386b7d7e1e 100644 --- a/openpype/tools/mayalookassigner/models.py +++ b/openpype/tools/mayalookassigner/models.py @@ -1,8 +1,8 @@ from collections import defaultdict from Qt import QtCore +import qtawesome -from avalon.vendor import qtawesome from avalon.style import colors from openpype.tools.utils import models diff --git a/openpype/tools/project_manager/project_manager/style.py b/openpype/tools/project_manager/project_manager/style.py index d24fc7102f..4405d05960 100644 --- a/openpype/tools/project_manager/project_manager/style.py +++ b/openpype/tools/project_manager/project_manager/style.py @@ -1,7 +1,7 @@ import os -from Qt import QtCore, QtGui +from Qt import QtGui -from avalon.vendor import qtawesome +import qtawesome from openpype.tools.utils import paint_image_with_color diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index fb1f0e54aa..9a9fe3193e 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -4,8 +4,7 @@ import re import copy import collections from Qt import QtWidgets, QtCore, QtGui - -from avalon.vendor import qtawesome +import qtawesome from openpype.widgets.attribute_defs import create_widget_for_attr_def from openpype.tools import resources diff --git a/openpype/tools/pyblish_pype/model.py b/openpype/tools/pyblish_pype/model.py index 0faadb5940..2931a379b3 100644 --- a/openpype/tools/pyblish_pype/model.py +++ b/openpype/tools/pyblish_pype/model.py @@ -29,10 +29,9 @@ import pyblish from . import settings, util from .awesome import tags as awesome -import Qt from Qt import QtCore, QtGui +import qtawesome from six import text_type -from .vendor import qtawesome from .constants import PluginStates, InstanceStates, GroupStates, Roles from openpype.api import get_system_settings diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 6435e5c488..cba60be355 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -4,9 +4,9 @@ import logging from collections import defaultdict from Qt import QtCore, QtGui -from avalon import api, io, style, schema -from avalon.vendor import qtawesome +import qtawesome +from avalon import api, io, style, schema from avalon.lib import HeroVersionType from openpype.tools.utils.models import TreeModel, Item diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index 4946c073d4..93ea68beb4 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -1,9 +1,9 @@ import collections import logging from Qt import QtWidgets, QtCore +import qtawesome from avalon import io, api, pipeline -from avalon.vendor import qtawesome from .widgets import ( ButtonWithMenu, diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index ec48b10e47..32c1883de6 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -3,9 +3,9 @@ import logging from functools import partial from Qt import QtWidgets, QtCore +import qtawesome from avalon import io, api, style -from avalon.vendor import qtawesome from avalon.lib import HeroVersionType from openpype.modules import ModulesManager diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 095d30cac0..83e4435015 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -2,7 +2,7 @@ import os import sys from Qt import QtWidgets, QtCore -from avalon.vendor import qtawesome +import qtawesome from avalon import io, api from openpype import style diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 663d497c36..a5b5cd40f0 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -1,9 +1,9 @@ -import os import sys import traceback import contextlib from enum import Enum from Qt import QtWidgets, QtCore +import qtawesome from openpype.lib import get_openpype_version from openpype.tools.utils import set_style_property @@ -63,7 +63,6 @@ from .item_widgets import ( PathInputWidget ) from .color_widget import ColorWidget -from avalon.vendor import qtawesome class CategoryState(Enum): diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index f793aab057..577c2630ab 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -2,7 +2,7 @@ import os import copy import uuid from Qt import QtWidgets, QtCore, QtGui -from avalon.vendor import qtawesome +import qtawesome from avalon.mongodb import ( AvalonMongoConnection, AvalonMongoDB diff --git a/openpype/tools/standalonepublish/widgets/model_asset.py b/openpype/tools/standalonepublish/widgets/model_asset.py index 60afe8f96c..6d764eff9f 100644 --- a/openpype/tools/standalonepublish/widgets/model_asset.py +++ b/openpype/tools/standalonepublish/widgets/model_asset.py @@ -1,8 +1,8 @@ import logging import collections from Qt import QtCore, QtGui +import qtawesome from . import TreeModel, Node -from avalon.vendor import qtawesome from avalon import style diff --git a/openpype/tools/standalonepublish/widgets/model_tasks_template.py b/openpype/tools/standalonepublish/widgets/model_tasks_template.py index 476f45391d..1f36eaa39d 100644 --- a/openpype/tools/standalonepublish/widgets/model_tasks_template.py +++ b/openpype/tools/standalonepublish/widgets/model_tasks_template.py @@ -1,6 +1,6 @@ from Qt import QtCore +import qtawesome from . import Node, TreeModel -from avalon.vendor import qtawesome from avalon import style diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 2886d600bf..d929f227f9 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -1,9 +1,9 @@ import contextlib from Qt import QtWidgets, QtCore +import qtawesome from openpype.tools.utils import PlaceholderLineEdit -from avalon.vendor import qtawesome from avalon import style from . import RecursiveSortFilterProxyModel, AssetModel diff --git a/openpype/tools/standalonepublish/widgets/widget_family_desc.py b/openpype/tools/standalonepublish/widgets/widget_family_desc.py index 8c95ddf2e4..79681615b9 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family_desc.py +++ b/openpype/tools/standalonepublish/widgets/widget_family_desc.py @@ -1,7 +1,7 @@ -from Qt import QtWidgets, QtCore, QtGui -from . import FamilyRole, PluginRole -from avalon.vendor import qtawesome import six +from Qt import QtWidgets, QtCore, QtGui +import qtawesome +from . import FamilyRole, PluginRole class FamilyDescriptionWidget(QtWidgets.QWidget): diff --git a/openpype/tools/subsetmanager/window.py b/openpype/tools/subsetmanager/window.py index b7430d0626..a53af52174 100644 --- a/openpype/tools/subsetmanager/window.py +++ b/openpype/tools/subsetmanager/window.py @@ -2,9 +2,9 @@ import os import sys from Qt import QtWidgets, QtCore +import qtawesome from avalon import api -from avalon.vendor import qtawesome from openpype import style from openpype.tools.utils import PlaceholderLineEdit diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index 17164d9e0f..d410b0f1c3 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -3,9 +3,9 @@ import collections import Qt from Qt import QtWidgets, QtCore, QtGui +import qtawesome from avalon import style -from avalon.vendor import qtawesome from openpype.style import get_objected_colors from openpype.tools.flickcharm import FlickCharm diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 01b9e25ef3..1cbc632804 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -4,10 +4,10 @@ import contextlib import collections from Qt import QtWidgets, QtCore, QtGui +import qtawesome import avalon.api from avalon import style -from avalon.vendor import qtawesome from openpype.api import ( get_project_settings, diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py index 2a8a45626c..7619f59974 100644 --- a/openpype/tools/utils/tasks_widget.py +++ b/openpype/tools/utils/tasks_widget.py @@ -1,7 +1,7 @@ from Qt import QtWidgets, QtCore, QtGui +import qtawesome from avalon import style -from avalon.vendor import qtawesome from .views import DeselectableTreeView @@ -14,6 +14,7 @@ TASK_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 4 class TasksModel(QtGui.QStandardItemModel): """A model listing the tasks combined for a list of assets""" + def __init__(self, dbcon, parent=None): super(TasksModel, self).__init__(parent=parent) self.dbcon = dbcon diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 783736a9ca..d5ae909be8 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -2,7 +2,7 @@ import logging from Qt import QtWidgets, QtCore, QtGui import qargparse -from avalon.vendor import qtawesome +import qtawesome from openpype.style import ( get_objected_colors, get_style_image_path diff --git a/openpype/tools/workfiles/model.py b/openpype/tools/workfiles/model.py index 3425cc3df0..b3cf5063e7 100644 --- a/openpype/tools/workfiles/model.py +++ b/openpype/tools/workfiles/model.py @@ -2,9 +2,9 @@ import os import logging from Qt import QtCore +import qtawesome from avalon import style -from avalon.vendor import qtawesome from openpype.tools.utils.models import TreeModel, Item log = logging.getLogger(__name__) diff --git a/poetry.lock b/poetry.lock index b6eba33e0a..a6507bb358 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1219,6 +1219,26 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "qtawesome" +version = "0.7.3" +description = "FontAwesome icons in PyQt and PySide applications" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +qtpy = "*" +six = "*" + +[[package]] +name = "qtpy" +version = "1.11.3" +description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5, PyQt4 and PySide) and additional custom QWidgets." +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" + [[package]] name = "recommonmark" version = "0.7.1" @@ -2651,6 +2671,14 @@ pywin32-ctypes = [ {file = "Qt.py-1.3.6-py2.py3-none-any.whl", hash = "sha256:7edf6048d07a6924707506b5ba34a6e05d66dde9a3f4e3a62f9996ccab0b91c7"}, {file = "Qt.py-1.3.6.tar.gz", hash = "sha256:0d78656a2f814602eee304521c7bf5da0cec414818b3833712c77524294c404a"}, ] +qtawesome = [ + {file = "QtAwesome-0.7.3-py2.py3-none-any.whl", hash = "sha256:ddf4530b4af71cec13b24b88a4cdb56ec85b1e44c43c42d0698804c7137b09b0"}, + {file = "QtAwesome-0.7.3.tar.gz", hash = "sha256:b98b9038d19190e83ab26d91c4d8fc3a36591ee2bc7f5016d4438b8240d097bd"}, +] +qtpy = [ + {file = "QtPy-1.11.3-py2.py3-none-any.whl", hash = "sha256:e121fbee8e95645af29c5a4aceba8d657991551fc1aa3b6b6012faf4725a1d20"}, + {file = "QtPy-1.11.3.tar.gz", hash = "sha256:d427addd37386a8d786db81864a5536700861d95bf085cb31d1bea855d699557"}, +] recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, diff --git a/pyproject.toml b/pyproject.toml index 2469cb76a9..106ae788f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,8 @@ pyblish-base = "^1.8.8" pynput = "^1.7.2" # idle manager in tray pymongo = "^3.11.2" "Qt.py" = "^1.3.3" +qtpy = "^1.11.3" +qtawesome = "0.7.3" speedcopy = "^2.1" six = "^1.15" semver = "^2.13.0" # for version resolution From abe340130fb71844b1a7ffe6c6878f0c5b9cf563 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 17:49:15 +0100 Subject: [PATCH 05/29] fixed remaining imports from avalon.vendor --- .../celaction/plugins/publish/submit_celaction_deadline.py | 4 ++-- openpype/hosts/maya/plugins/load/load_vdb_to_vray.py | 2 +- openpype/modules/log_viewer/tray/widgets.py | 2 +- openpype/modules/sync_server/tray/models.py | 2 +- openpype/modules/sync_server/tray/widgets.py | 2 +- openpype/tools/assetcreator/model.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/celaction/plugins/publish/submit_celaction_deadline.py b/openpype/hosts/celaction/plugins/publish/submit_celaction_deadline.py index fd958d11a3..ea109e9445 100644 --- a/openpype/hosts/celaction/plugins/publish/submit_celaction_deadline.py +++ b/openpype/hosts/celaction/plugins/publish/submit_celaction_deadline.py @@ -1,9 +1,9 @@ import os +import re import json import getpass -from avalon.vendor import requests -import re +import requests import pyblish.api diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py index 099c020093..6d5544103d 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -174,7 +174,7 @@ class LoadVDBtoVRay(api.Loader): fname = files[0] else: # Sequence - from avalon.vendor import clique + import clique # todo: check support for negative frames as input collections, remainder = clique.assemble(files) assert len(collections) == 1, ( diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index 5a67780413..ff77405de5 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -1,5 +1,5 @@ from Qt import QtCore, QtWidgets -from avalon.vendor import qtawesome +import qtawesome from .models import LogModel, LogsFilterProxy diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 80f41992cb..7241cc3472 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -4,9 +4,9 @@ from bson.objectid import ObjectId from Qt import QtCore from Qt.QtCore import Qt +import qtawesome from openpype.tools.utils.delegates import pretty_timestamp -from avalon.vendor import qtawesome from openpype.lib import PypeLogger from openpype.api import get_local_site_id diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 18487b3d11..6aae9562cf 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -5,6 +5,7 @@ from functools import partial from Qt import QtWidgets, QtCore, QtGui from Qt.QtCore import Qt +import qtawesome from openpype.tools.settings import style @@ -12,7 +13,6 @@ from openpype.api import get_local_site_id from openpype.lib import PypeLogger from openpype.tools.utils.delegates import pretty_timestamp -from avalon.vendor import qtawesome from .models import ( SyncRepresentationSummaryModel, diff --git a/openpype/tools/assetcreator/model.py b/openpype/tools/assetcreator/model.py index f84541ca2a..ae9e0f673a 100644 --- a/openpype/tools/assetcreator/model.py +++ b/openpype/tools/assetcreator/model.py @@ -2,7 +2,7 @@ import re import logging from Qt import QtCore, QtWidgets -from avalon.vendor import qtawesome +import qtawesome from avalon import io from avalon import style From f021eaf389053394972d4babd2a076c7d8c5180e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 18:08:25 +0100 Subject: [PATCH 06/29] added python 2 dependency functools32 --- .../python/python_2/functools32/__init__.py | 1 + .../python_2/functools32/_dummy_thread32.py | 158 +++++++ .../python_2/functools32/functools32.py | 423 ++++++++++++++++++ .../python/python_2/functools32/reprlib32.py | 157 +++++++ 4 files changed, 739 insertions(+) create mode 100644 openpype/vendor/python/python_2/functools32/__init__.py create mode 100644 openpype/vendor/python/python_2/functools32/_dummy_thread32.py create mode 100644 openpype/vendor/python/python_2/functools32/functools32.py create mode 100644 openpype/vendor/python/python_2/functools32/reprlib32.py diff --git a/openpype/vendor/python/python_2/functools32/__init__.py b/openpype/vendor/python/python_2/functools32/__init__.py new file mode 100644 index 0000000000..837f7fb651 --- /dev/null +++ b/openpype/vendor/python/python_2/functools32/__init__.py @@ -0,0 +1 @@ +from .functools32 import * diff --git a/openpype/vendor/python/python_2/functools32/_dummy_thread32.py b/openpype/vendor/python/python_2/functools32/_dummy_thread32.py new file mode 100644 index 0000000000..8503b0e3dd --- /dev/null +++ b/openpype/vendor/python/python_2/functools32/_dummy_thread32.py @@ -0,0 +1,158 @@ +"""Drop-in replacement for the thread module. + +Meant to be used as a brain-dead substitute so that threaded code does +not need to be rewritten for when the thread module is not present. + +Suggested usage is:: + + try: + try: + import _thread # Python >= 3 + except: + import thread as _thread # Python < 3 + except ImportError: + import _dummy_thread as _thread + +""" +# Exports only things specified by thread documentation; +# skipping obsolete synonyms allocate(), start_new(), exit_thread(). +__all__ = ['error', 'start_new_thread', 'exit', 'get_ident', 'allocate_lock', + 'interrupt_main', 'LockType'] + +# A dummy value +TIMEOUT_MAX = 2**31 + +# NOTE: this module can be imported early in the extension building process, +# and so top level imports of other modules should be avoided. Instead, all +# imports are done when needed on a function-by-function basis. Since threads +# are disabled, the import lock should not be an issue anyway (??). + +class error(Exception): + """Dummy implementation of _thread.error.""" + + def __init__(self, *args): + self.args = args + +def start_new_thread(function, args, kwargs={}): + """Dummy implementation of _thread.start_new_thread(). + + Compatibility is maintained by making sure that ``args`` is a + tuple and ``kwargs`` is a dictionary. If an exception is raised + and it is SystemExit (which can be done by _thread.exit()) it is + caught and nothing is done; all other exceptions are printed out + by using traceback.print_exc(). + + If the executed function calls interrupt_main the KeyboardInterrupt will be + raised when the function returns. + + """ + if type(args) != type(tuple()): + raise TypeError("2nd arg must be a tuple") + if type(kwargs) != type(dict()): + raise TypeError("3rd arg must be a dict") + global _main + _main = False + try: + function(*args, **kwargs) + except SystemExit: + pass + except: + import traceback + traceback.print_exc() + _main = True + global _interrupt + if _interrupt: + _interrupt = False + raise KeyboardInterrupt + +def exit(): + """Dummy implementation of _thread.exit().""" + raise SystemExit + +def get_ident(): + """Dummy implementation of _thread.get_ident(). + + Since this module should only be used when _threadmodule is not + available, it is safe to assume that the current process is the + only thread. Thus a constant can be safely returned. + """ + return -1 + +def allocate_lock(): + """Dummy implementation of _thread.allocate_lock().""" + return LockType() + +def stack_size(size=None): + """Dummy implementation of _thread.stack_size().""" + if size is not None: + raise error("setting thread stack size not supported") + return 0 + +class LockType(object): + """Class implementing dummy implementation of _thread.LockType. + + Compatibility is maintained by maintaining self.locked_status + which is a boolean that stores the state of the lock. Pickling of + the lock, though, should not be done since if the _thread module is + then used with an unpickled ``lock()`` from here problems could + occur from this class not having atomic methods. + + """ + + def __init__(self): + self.locked_status = False + + def acquire(self, waitflag=None, timeout=-1): + """Dummy implementation of acquire(). + + For blocking calls, self.locked_status is automatically set to + True and returned appropriately based on value of + ``waitflag``. If it is non-blocking, then the value is + actually checked and not set if it is already acquired. This + is all done so that threading.Condition's assert statements + aren't triggered and throw a little fit. + + """ + if waitflag is None or waitflag: + self.locked_status = True + return True + else: + if not self.locked_status: + self.locked_status = True + return True + else: + if timeout > 0: + import time + time.sleep(timeout) + return False + + __enter__ = acquire + + def __exit__(self, typ, val, tb): + self.release() + + def release(self): + """Release the dummy lock.""" + # XXX Perhaps shouldn't actually bother to test? Could lead + # to problems for complex, threaded code. + if not self.locked_status: + raise error + self.locked_status = False + return True + + def locked(self): + return self.locked_status + +# Used to signal that interrupt_main was called in a "thread" +_interrupt = False +# True when not executing in a "thread" +_main = True + +def interrupt_main(): + """Set _interrupt flag to True to have start_new_thread raise + KeyboardInterrupt upon exiting.""" + if _main: + raise KeyboardInterrupt + else: + global _interrupt + _interrupt = True diff --git a/openpype/vendor/python/python_2/functools32/functools32.py b/openpype/vendor/python/python_2/functools32/functools32.py new file mode 100644 index 0000000000..c44551fac0 --- /dev/null +++ b/openpype/vendor/python/python_2/functools32/functools32.py @@ -0,0 +1,423 @@ +"""functools.py - Tools for working with functions and callable objects +""" +# Python module wrapper for _functools C module +# to allow utilities written in Python to be added +# to the functools module. +# Written by Nick Coghlan +# and Raymond Hettinger +# Copyright (C) 2006-2010 Python Software Foundation. +# See C source code for _functools credits/copyright + +__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES', + 'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial'] + +from _functools import partial, reduce +from collections import MutableMapping, namedtuple +from .reprlib32 import recursive_repr as _recursive_repr +from weakref import proxy as _proxy +import sys as _sys +try: + from thread import allocate_lock as Lock +except ImportError: + from ._dummy_thread32 import allocate_lock as Lock + +################################################################################ +### OrderedDict +################################################################################ + +class _Link(object): + __slots__ = 'prev', 'next', 'key', '__weakref__' + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as regular dictionaries. + + # The internal self.__map dict maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # The sentinel is in self.__hardroot with a weakref proxy in self.__root. + # The prev links are weakref proxies (to prevent circular references). + # Individual links are kept alive by the hard reference in self.__map. + # Those hard references disappear when a key is deleted from an OrderedDict. + + def __init__(self, *args, **kwds): + '''Initialize an ordered dictionary. The signature is the same as + regular dictionaries, but keyword arguments are not recommended because + their insertion order is arbitrary. + + ''' + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__hardroot = _Link() + self.__root = root = _proxy(self.__hardroot) + root.prev = root.next = root + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, + dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link at the end of the linked list, + # and the inherited dictionary is updated with the new key/value pair. + if key not in self: + self.__map[key] = link = Link() + root = self.__root + last = root.prev + link.prev, link.next, link.key = last, root, key + last.next = link + root.prev = proxy(link) + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which gets + # removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link = self.__map.pop(key) + link_prev = link.prev + link_next = link.next + link_prev.next = link_next + link_next.prev = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + # Traverse the linked list in order. + root = self.__root + curr = root.next + while curr is not root: + yield curr.key + curr = curr.next + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + # Traverse the linked list in reverse order. + root = self.__root + curr = root.prev + while curr is not root: + yield curr.key + curr = curr.prev + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + root = self.__root + root.prev = root.next = root + self.__map.clear() + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root.prev + link_prev = link.prev + link_prev.next = root + root.prev = link_prev + else: + link = root.next + link_next = link.next + root.next = link_next + link_next.prev = root + key = link.key + del self.__map[key] + value = dict.pop(self, key) + return key, value + + def move_to_end(self, key, last=True): + '''Move an existing element to the end (or beginning if last==False). + + Raises KeyError if the element does not exist. + When last=True, acts like a fast version of self[key]=self.pop(key). + + ''' + link = self.__map[key] + link_prev = link.prev + link_next = link.next + link_prev.next = link_next + link_next.prev = link_prev + root = self.__root + if last: + last = root.prev + link.prev = last + link.next = root + last.next = root.prev = link + else: + first = root.next + link.prev = root + link.next = first + root.next = first.prev = link + + def __sizeof__(self): + sizeof = _sys.getsizeof + n = len(self) + 1 # number of links including root + size = sizeof(self.__dict__) # instance dictionary + size += sizeof(self.__map) * 2 # internal dict and inherited dict + size += sizeof(self.__hardroot) * n # link objects + size += sizeof(self.__root) * n # proxy objects + return size + + update = __update = MutableMapping.update + keys = MutableMapping.keys + values = MutableMapping.values + items = MutableMapping.items + __ne__ = MutableMapping.__ne__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding + value. If key is not found, d is returned if given, otherwise KeyError + is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + @_recursive_repr() + def __repr__(self): + 'od.__repr__() <==> repr(od)' + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self.items())) + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S. + If not specified, the value defaults to None. + + ''' + self = cls() + for key in iterable: + self[key] = value + return self + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return len(self)==len(other) and \ + all(p==q for p, q in zip(self.items(), other.items())) + return dict.__eq__(self, other) + +# update_wrapper() and wraps() are tools to help write +# wrapper functions that can handle naive introspection + +WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__') +WRAPPER_UPDATES = ('__dict__',) +def update_wrapper(wrapper, + wrapped, + assigned = WRAPPER_ASSIGNMENTS, + updated = WRAPPER_UPDATES): + """Update a wrapper function to look like the wrapped function + + wrapper is the function to be updated + wrapped is the original function + assigned is a tuple naming the attributes assigned directly + from the wrapped function to the wrapper function (defaults to + functools.WRAPPER_ASSIGNMENTS) + updated is a tuple naming the attributes of the wrapper that + are updated with the corresponding attribute from the wrapped + function (defaults to functools.WRAPPER_UPDATES) + """ + wrapper.__wrapped__ = wrapped + for attr in assigned: + try: + value = getattr(wrapped, attr) + except AttributeError: + pass + else: + setattr(wrapper, attr, value) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, {})) + # Return the wrapper so this can be used as a decorator via partial() + return wrapper + +def wraps(wrapped, + assigned = WRAPPER_ASSIGNMENTS, + updated = WRAPPER_UPDATES): + """Decorator factory to apply update_wrapper() to a wrapper function + + Returns a decorator that invokes update_wrapper() with the decorated + function as the wrapper argument and the arguments to wraps() as the + remaining arguments. Default arguments are as for update_wrapper(). + This is a convenience function to simplify applying partial() to + update_wrapper(). + """ + return partial(update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + +def total_ordering(cls): + """Class decorator that fills in missing ordering methods""" + convert = { + '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)), + ('__le__', lambda self, other: self < other or self == other), + ('__ge__', lambda self, other: not self < other)], + '__le__': [('__ge__', lambda self, other: not self <= other or self == other), + ('__lt__', lambda self, other: self <= other and not self == other), + ('__gt__', lambda self, other: not self <= other)], + '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)), + ('__ge__', lambda self, other: self > other or self == other), + ('__le__', lambda self, other: not self > other)], + '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other), + ('__gt__', lambda self, other: self >= other and not self == other), + ('__lt__', lambda self, other: not self >= other)] + } + roots = set(dir(cls)) & set(convert) + if not roots: + raise ValueError('must define at least one ordering operation: < > <= >=') + root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ + for opname, opfunc in convert[root]: + if opname not in roots: + opfunc.__name__ = opname + opfunc.__doc__ = getattr(int, opname).__doc__ + setattr(cls, opname, opfunc) + return cls + +def cmp_to_key(mycmp): + """Convert a cmp= function into a key= function""" + class K(object): + __slots__ = ['obj'] + def __init__(self, obj): + self.obj = obj + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 + __hash__ = None + return K + +_CacheInfo = namedtuple("CacheInfo", "hits misses maxsize currsize") + +def lru_cache(maxsize=100): + """Least-recently-used cache decorator. + + If *maxsize* is set to None, the LRU features are disabled and the cache + can grow without bound. + + Arguments to the cached function must be hashable. + + View the cache statistics named tuple (hits, misses, maxsize, currsize) with + f.cache_info(). Clear the cache and statistics with f.cache_clear(). + Access the underlying function with f.__wrapped__. + + See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used + + """ + # Users should only access the lru_cache through its public API: + # cache_info, cache_clear, and f.__wrapped__ + # The internals of the lru_cache are encapsulated for thread safety and + # to allow the implementation to change (including a possible C version). + + def decorating_function(user_function, + tuple=tuple, sorted=sorted, len=len, KeyError=KeyError): + + hits, misses = [0], [0] + kwd_mark = (object(),) # separates positional and keyword args + lock = Lock() # needed because OrderedDict isn't threadsafe + + if maxsize is None: + cache = dict() # simple cache without ordering or size limit + + @wraps(user_function) + def wrapper(*args, **kwds): + key = args + if kwds: + key += kwd_mark + tuple(sorted(kwds.items())) + try: + result = cache[key] + hits[0] += 1 + return result + except KeyError: + pass + result = user_function(*args, **kwds) + cache[key] = result + misses[0] += 1 + return result + else: + cache = OrderedDict() # ordered least recent to most recent + cache_popitem = cache.popitem + cache_renew = cache.move_to_end + + @wraps(user_function) + def wrapper(*args, **kwds): + key = args + if kwds: + key += kwd_mark + tuple(sorted(kwds.items())) + with lock: + try: + result = cache[key] + cache_renew(key) # record recent use of this key + hits[0] += 1 + return result + except KeyError: + pass + result = user_function(*args, **kwds) + with lock: + cache[key] = result # record recent use of this key + misses[0] += 1 + if len(cache) > maxsize: + cache_popitem(0) # purge least recently used cache entry + return result + + def cache_info(): + """Report cache statistics""" + with lock: + return _CacheInfo(hits[0], misses[0], maxsize, len(cache)) + + def cache_clear(): + """Clear the cache and cache statistics""" + with lock: + cache.clear() + hits[0] = misses[0] = 0 + + wrapper.cache_info = cache_info + wrapper.cache_clear = cache_clear + return wrapper + + return decorating_function diff --git a/openpype/vendor/python/python_2/functools32/reprlib32.py b/openpype/vendor/python/python_2/functools32/reprlib32.py new file mode 100644 index 0000000000..af919758ca --- /dev/null +++ b/openpype/vendor/python/python_2/functools32/reprlib32.py @@ -0,0 +1,157 @@ +"""Redo the builtin repr() (representation) but with limits on most sizes.""" + +__all__ = ["Repr", "repr", "recursive_repr"] + +import __builtin__ as builtins +from itertools import islice +try: + from thread import get_ident +except ImportError: + from _dummy_thread32 import get_ident + +def recursive_repr(fillvalue='...'): + 'Decorator to make a repr function return fillvalue for a recursive call' + + def decorating_function(user_function): + repr_running = set() + + def wrapper(self): + key = id(self), get_ident() + if key in repr_running: + return fillvalue + repr_running.add(key) + try: + result = user_function(self) + finally: + repr_running.discard(key) + return result + + # Can't use functools.wraps() here because of bootstrap issues + wrapper.__module__ = getattr(user_function, '__module__') + wrapper.__doc__ = getattr(user_function, '__doc__') + wrapper.__name__ = getattr(user_function, '__name__') + wrapper.__annotations__ = getattr(user_function, '__annotations__', {}) + return wrapper + + return decorating_function + +class Repr: + + def __init__(self): + self.maxlevel = 6 + self.maxtuple = 6 + self.maxlist = 6 + self.maxarray = 5 + self.maxdict = 4 + self.maxset = 6 + self.maxfrozenset = 6 + self.maxdeque = 6 + self.maxstring = 30 + self.maxlong = 40 + self.maxother = 30 + + def repr(self, x): + return self.repr1(x, self.maxlevel) + + def repr1(self, x, level): + typename = type(x).__name__ + if ' ' in typename: + parts = typename.split() + typename = '_'.join(parts) + if hasattr(self, 'repr_' + typename): + return getattr(self, 'repr_' + typename)(x, level) + else: + return self.repr_instance(x, level) + + def _repr_iterable(self, x, level, left, right, maxiter, trail=''): + n = len(x) + if level <= 0 and n: + s = '...' + else: + newlevel = level - 1 + repr1 = self.repr1 + pieces = [repr1(elem, newlevel) for elem in islice(x, maxiter)] + if n > maxiter: pieces.append('...') + s = ', '.join(pieces) + if n == 1 and trail: right = trail + right + return '%s%s%s' % (left, s, right) + + def repr_tuple(self, x, level): + return self._repr_iterable(x, level, '(', ')', self.maxtuple, ',') + + def repr_list(self, x, level): + return self._repr_iterable(x, level, '[', ']', self.maxlist) + + def repr_array(self, x, level): + header = "array('%s', [" % x.typecode + return self._repr_iterable(x, level, header, '])', self.maxarray) + + def repr_set(self, x, level): + x = _possibly_sorted(x) + return self._repr_iterable(x, level, 'set([', '])', self.maxset) + + def repr_frozenset(self, x, level): + x = _possibly_sorted(x) + return self._repr_iterable(x, level, 'frozenset([', '])', + self.maxfrozenset) + + def repr_deque(self, x, level): + return self._repr_iterable(x, level, 'deque([', '])', self.maxdeque) + + def repr_dict(self, x, level): + n = len(x) + if n == 0: return '{}' + if level <= 0: return '{...}' + newlevel = level - 1 + repr1 = self.repr1 + pieces = [] + for key in islice(_possibly_sorted(x), self.maxdict): + keyrepr = repr1(key, newlevel) + valrepr = repr1(x[key], newlevel) + pieces.append('%s: %s' % (keyrepr, valrepr)) + if n > self.maxdict: pieces.append('...') + s = ', '.join(pieces) + return '{%s}' % (s,) + + def repr_str(self, x, level): + s = builtins.repr(x[:self.maxstring]) + if len(s) > self.maxstring: + i = max(0, (self.maxstring-3)//2) + j = max(0, self.maxstring-3-i) + s = builtins.repr(x[:i] + x[len(x)-j:]) + s = s[:i] + '...' + s[len(s)-j:] + return s + + def repr_int(self, x, level): + s = builtins.repr(x) # XXX Hope this isn't too slow... + if len(s) > self.maxlong: + i = max(0, (self.maxlong-3)//2) + j = max(0, self.maxlong-3-i) + s = s[:i] + '...' + s[len(s)-j:] + return s + + def repr_instance(self, x, level): + try: + s = builtins.repr(x) + # Bugs in x.__repr__() can cause arbitrary + # exceptions -- then make up something + except Exception: + return '<%s instance at %x>' % (x.__class__.__name__, id(x)) + if len(s) > self.maxother: + i = max(0, (self.maxother-3)//2) + j = max(0, self.maxother-3-i) + s = s[:i] + '...' + s[len(s)-j:] + return s + + +def _possibly_sorted(x): + # Since not all sequences of items can be sorted and comparison + # functions may raise arbitrary exceptions, return an unsorted + # sequence in that case. + try: + return sorted(x) + except Exception: + return list(x) + +aRepr = Repr() +repr = aRepr.repr From 04ede4539d4896eb6c87ebf3f2a7b7caa977e8a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 18:14:31 +0100 Subject: [PATCH 07/29] lower jsonschema module version --- poetry.lock | 6 +++--- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index a6507bb358..ee7b839b8d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -674,7 +674,7 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "jsonschema" -version = "3.2.0" +version = "2.6.0" description = "An implementation of JSON Schema validation for Python" category = "main" optional = false @@ -2121,8 +2121,8 @@ jinxed = [ {file = "jinxed-1.1.0.tar.gz", hash = "sha256:d8f1731f134e9e6b04d95095845ae6c10eb15cb223a5f0cabdea87d4a279c305"}, ] jsonschema = [ - {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, - {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, + {file = "jsonschema-2.6.0-py2.py3-none-any.whl", hash = "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08"}, + {file = "jsonschema-2.6.0.tar.gz", hash = "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"}, ] keyring = [ {file = "keyring-22.4.0-py3-none-any.whl", hash = "sha256:d6c531f6d12f3304db6029af1d19894bd446ecbbadd22465fa0f096b3e12d258"}, diff --git a/pyproject.toml b/pyproject.toml index 106ae788f9..2b30d92cdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ Click = "^7" dnspython = "^2.1.0" ftrack-python-api = "2.0.*" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) -jsonschema = "^3.2.0" +jsonschema = "^2.6.0" keyring = "^22.0.1" log4mongo = "^1.7" pathlib2= "^2.3.5" # deadline submit publish job only (single place, maybe not needed?) From 413e03cae155de3dbe9e4dce2c4d30ec46d237a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 15:54:16 +0100 Subject: [PATCH 08/29] moved 'create_hard_link' to path_tools --- openpype/lib/__init__.py | 4 ++-- openpype/lib/path_tools.py | 35 ++++++++++++++++++++++++++++++++ openpype/lib/vendor_bin_utils.py | 35 -------------------------------- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index c4097086e0..34b217f690 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -17,7 +17,6 @@ site.addsitedir(python_version_dir) from .vendor_bin_utils import ( - create_hard_link, find_executable, get_vendor_bin_path, get_oiio_tools_path, @@ -160,6 +159,7 @@ from .plugin_tools import ( ) from .path_tools import ( + create_hard_link, version_up, get_version_from_path, get_last_version_from_path, @@ -210,7 +210,6 @@ __all__ = [ "get_paths_from_environ", "get_global_environments", - "create_hard_link", "get_vendor_bin_path", "get_oiio_tools_path", "get_ffmpeg_tool_path", @@ -293,6 +292,7 @@ __all__ = [ "get_unique_layer_name", "get_background_layers", + "create_hard_link", "version_up", "get_version_from_path", "get_last_version_from_path", diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 71fc0fe25c..c36e45c51f 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -13,6 +13,41 @@ from .profiles_filtering import filter_profiles log = logging.getLogger(__name__) +def create_hard_link(src_path, dst_path): + """Create hardlink of file. + + Args: + src_path(str): Full path to a file which is used as source for + hardlink. + dst_path(str): Full path to a file where a link of source will be + added. + """ + # Use `os.link` if is available + # - should be for all platforms with newer python versions + if hasattr(os, "link"): + os.link(src_path, dst_path) + return + + # Windows implementation of hardlinks + # - used in Python 2 + if platform.system().lower() == "windows": + import ctypes + from ctypes.wintypes import BOOL + CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW + CreateHardLink.argtypes = [ + ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p + ] + CreateHardLink.restype = BOOL + + res = CreateHardLink(dst_path, src_path, None) + if res == 0: + raise ctypes.WinError() + # Raises not implemented error if gets here + raise NotImplementedError( + "Implementation of hardlink for current environment is missing." + ) + + def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" return b.join(s.rsplit(a, n)) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 4a62da8f0c..4be016f656 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -86,41 +86,6 @@ def find_executable(executable): return None -def create_hard_link(src_path, dst_path): - """Create hardlink of file. - - Args: - src_path(str): Full path to a file which is used as source for - hardlink. - dst_path(str): Full path to a file where a link of source will be - added. - """ - # Use `os.link` if is available - # - should be for all platforms with newer python versions - if hasattr(os, "link"): - os.link(src_path, dst_path) - return - - # Windows implementation of hardlinks - # - used in Python 2 - if platform.system().lower() == "windows": - import ctypes - from ctypes.wintypes import BOOL - CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW - CreateHardLink.argtypes = [ - ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p - ] - CreateHardLink.restype = BOOL - - res = CreateHardLink(dst_path, src_path, None) - if res == 0: - raise ctypes.WinError() - # Raises not implemented error if gets here - raise NotImplementedError( - "Implementation of hardlink for current environment is missing." - ) - - def get_vendor_bin_path(bin_app): """Path to OpenPype vendorized binaries. From 295002d7543f9ac47896bb3c4cb78dc7e1691b08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 15:54:58 +0100 Subject: [PATCH 09/29] added missing platform --- openpype/lib/path_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index c36e45c51f..3a9f835272 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -4,6 +4,7 @@ import abc import json import logging import six +import platform from openpype.settings import get_project_settings From 7600590f7cb78db7f0e008337e2b3d77609cbb4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 17:31:16 +0100 Subject: [PATCH 10/29] moved avalon creators and added legacy prefix --- openpype/pipeline/__init__.py | 13 +- openpype/pipeline/create/__init__.py | 10 +- openpype/pipeline/create/legacy_create.py | 156 ++++++++++++++++++++++ 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 openpype/pipeline/create/legacy_create.py diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index e968df4011..9ac7d15d5b 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -4,7 +4,12 @@ from .create import ( BaseCreator, Creator, AutoCreator, - CreatedInstance + CreatedInstance, + + CreatorError, + + LegacyCreator, + legacy_create, ) from .publish import ( @@ -22,6 +27,12 @@ __all__ = ( "AutoCreator", "CreatedInstance", + "CreatorError", + + # Legacy creation + "LegacyCreator", + "legacy_create", + "PublishValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin" diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 948b719851..9571f56b8f 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -14,6 +14,11 @@ from .context import ( CreateContext ) +from .legacy_create import ( + LegacyCreator, + legacy_create, +) + __all__ = ( "SUBSET_NAME_ALLOWED_SYMBOLS", @@ -25,5 +30,8 @@ __all__ = ( "AutoCreator", "CreatedInstance", - "CreateContext" + "CreateContext", + + "LegacyCreator", + "legacy_create", ) diff --git a/openpype/pipeline/create/legacy_create.py b/openpype/pipeline/create/legacy_create.py new file mode 100644 index 0000000000..d05cdff689 --- /dev/null +++ b/openpype/pipeline/create/legacy_create.py @@ -0,0 +1,156 @@ +"""Create workflow moved from avalon-core repository. + +Renamed classes and functions +- 'Creator' -> 'LegacyCreator' +- 'create' -> 'legacy_create' +""" + +import logging +import collections + +from openpype.lib import get_subset_name + + +class LegacyCreator(object): + """Determine how assets are created""" + label = None + family = None + defaults = None + maintain_selection = True + + dynamic_subset_keys = [] + + log = logging.getLogger("LegacyCreator") + + def __init__(self, name, asset, options=None, data=None): + self.name = name # For backwards compatibility + self.options = options + + # Default data + self.data = collections.OrderedDict() + self.data["id"] = "pyblish.avalon.instance" + self.data["family"] = self.family + self.data["asset"] = asset + self.data["subset"] = name + self.data["active"] = True + + self.data.update(data or {}) + + def process(self): + pass + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + """Return dynamic data for current Creator plugin. + + By default return keys from `dynamic_subset_keys` attribute as mapping + to keep formatted template unchanged. + + ``` + dynamic_subset_keys = ["my_key"] + --- + output = { + "my_key": "{my_key}" + } + ``` + + Dynamic keys may override default Creator keys (family, task, asset, + ...) but do it wisely if you need. + + All of keys will be converted into 3 variants unchanged, capitalized + and all upper letters. Because of that are all keys lowered. + + This method can be modified to prefill some values just keep in mind it + is class method. + + Returns: + dict: Fill data for subset name template. + """ + dynamic_data = {} + for key in cls.dynamic_subset_keys: + key = key.lower() + dynamic_data[key] = "{" + key + "}" + return dynamic_data + + @classmethod + def get_subset_name( + cls, variant, task_name, asset_id, project_name, host_name=None + ): + """Return subset name created with entered arguments. + + Logic extracted from Creator tool. This method should give ability + to get subset name without the tool. + + TODO: Maybe change `variant` variable. + + By default is output concatenated family with user text. + + Args: + variant (str): What is entered by user in creator tool. + task_name (str): Context's task name. + asset_id (ObjectId): Mongo ID of context's asset. + project_name (str): Context's project name. + host_name (str): Name of host. + + Returns: + str: Formatted subset name with entered arguments. Should match + config's logic. + """ + + dynamic_data = cls.get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + + return get_subset_name( + cls.family, + variant, + task_name, + asset_id, + project_name, + host_name, + dynamic_data=dynamic_data + ) + + +def legacy_create(Creator, name, asset, options=None, data=None): + """Create a new instance + + Associate nodes with a subset and family. These nodes are later + validated, according to their `family`, and integrated into the + shared environment, relative their `subset`. + + Data relative each family, along with default data, are imprinted + into the resulting objectSet. This data is later used by extractors + and finally asset browsers to help identify the origin of the asset. + + Arguments: + Creator (Creator): Class of creator + name (str): Name of subset + asset (str): Name of asset + options (dict, optional): Additional options from GUI + data (dict, optional): Additional data from GUI + + Raises: + NameError on `subset` already exists + KeyError on invalid dynamic property + RuntimeError on host error + + Returns: + Name of instance + + """ + from avalon.api import registered_host + host = registered_host() + plugin = Creator(name, asset, options, data) + + if plugin.maintain_selection is True: + with host.maintained_selection(): + print("Running %s with maintained selection" % plugin) + instance = plugin.process() + return instance + + print("Running %s" % plugin) + instance = plugin.process() + return instance From d4f177f7bc4b6398240504c665777731b0dcb01f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 17:39:50 +0100 Subject: [PATCH 11/29] use moved create functions in hosts --- openpype/__init__.py | 3 +- openpype/api.py | 6 -- openpype/hosts/aftereffects/api/pipeline.py | 5 +- .../plugins/create/create_render.py | 7 +- openpype/hosts/blender/api/pipeline.py | 5 +- openpype/hosts/blender/api/plugin.py | 4 +- .../blender/plugins/load/load_layout_blend.py | 3 +- .../blender/plugins/load/load_layout_json.py | 4 +- .../hosts/blender/plugins/load/load_rig.py | 3 +- openpype/hosts/flame/api/pipeline.py | 5 +- openpype/hosts/flame/api/plugin.py | 3 +- openpype/hosts/fusion/api/pipeline.py | 5 +- .../fusion/plugins/create/create_exr_saver.py | 4 +- openpype/hosts/harmony/api/pipeline.py | 5 +- openpype/hosts/harmony/api/plugin.py | 5 +- openpype/hosts/hiero/api/pipeline.py | 5 +- openpype/hosts/hiero/api/plugin.py | 9 ++- openpype/hosts/houdini/api/pipeline.py | 3 +- openpype/hosts/houdini/api/plugin.py | 9 +-- openpype/hosts/maya/api/pipeline.py | 6 +- openpype/hosts/maya/api/plugin.py | 4 +- .../maya/plugins/create/create_render.py | 2 +- .../maya/plugins/create/create_vrayscene.py | 2 +- .../hosts/maya/plugins/load/load_reference.py | 3 +- openpype/hosts/nuke/api/pipeline.py | 5 +- openpype/hosts/nuke/api/plugin.py | 8 +-- openpype/hosts/photoshop/api/__init__.py | 3 +- openpype/hosts/photoshop/api/pipeline.py | 6 +- openpype/hosts/photoshop/api/plugin.py | 34 ---------- .../photoshop/plugins/create/create_image.py | 4 +- openpype/hosts/resolve/api/pipeline.py | 5 +- openpype/hosts/resolve/api/plugin.py | 3 +- openpype/hosts/tvpaint/api/pipeline.py | 5 +- openpype/hosts/tvpaint/api/plugin.py | 4 +- .../plugins/create/create_render_layer.py | 3 +- .../plugins/create/create_render_pass.py | 2 +- openpype/hosts/unreal/api/pipeline.py | 7 +- openpype/hosts/unreal/api/plugin.py | 4 +- openpype/hosts/webpublisher/api/__init__.py | 5 +- openpype/lib/plugin_tools.py | 2 +- openpype/plugin.py | 67 ------------------- openpype/tests/test_avalon_plugin_presets.py | 7 +- openpype/tools/creator/model.py | 3 +- openpype/tools/creator/window.py | 13 ++-- .../widgets/widget_family.py | 9 +-- 45 files changed, 111 insertions(+), 198 deletions(-) diff --git a/openpype/__init__.py b/openpype/__init__.py index 11b563ebfe..c41afaa47d 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -5,6 +5,7 @@ import platform import functools import logging +from openpype.pipeline import LegacyCreator from .settings import get_project_settings from .lib import ( Anatomy, @@ -113,7 +114,7 @@ def install(): pyblish.register_plugin_path(path) avalon.register_plugin_path(avalon.Loader, path) - avalon.register_plugin_path(avalon.Creator, path) + avalon.register_plugin_path(LegacyCreator, path) avalon.register_plugin_path(avalon.InventoryAction, path) # apply monkey patched discover to original one diff --git a/openpype/api.py b/openpype/api.py index 51854492ab..b692b36065 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -45,9 +45,6 @@ from .lib.avalon_context import ( from . import resources from .plugin import ( - PypeCreatorMixin, - Creator, - Extractor, ValidatePipelineOrder, @@ -89,9 +86,6 @@ __all__ = [ # Resources "resources", - # Pype creator mixin - "PypeCreatorMixin", - "Creator", # plugin classes "Extractor", # ordering diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 94f1e3d105..ef56e96155 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -9,6 +9,7 @@ from avalon import io, pipeline from openpype import lib from openpype.api import Logger +from openpype.pipeline import LegacyCreator import openpype.hosts.aftereffects from .launch_logic import get_stub @@ -66,7 +67,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( @@ -79,7 +80,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def on_pyblish_instance_toggled(instance, old_value, new_value): diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 8dfc85cdc8..41efb4b0ba 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,13 +1,12 @@ -from avalon.api import CreatorError - -import openpype.api +from openpype.pipeline import create +from openpype.pipeline import CreatorError from openpype.hosts.aftereffects.api import ( get_stub, list_instances ) -class CreateRender(openpype.api.Creator): +class CreateRender(create.LegacyCreator): """Render folder for publish. Creates subsets in format 'familyTaskSubsetname', diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 6da0ba3dcb..1c9820ff22 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -14,6 +14,7 @@ import avalon.api from avalon import io, schema from avalon.pipeline import AVALON_CONTAINER_ID +from openpype.pipeline import LegacyCreator from openpype.api import Logger import openpype.hosts.blender @@ -46,7 +47,7 @@ def install(): pyblish.api.register_plugin_path(str(PUBLISH_PATH)) avalon.api.register_plugin_path(avalon.api.Loader, str(LOAD_PATH)) - avalon.api.register_plugin_path(avalon.api.Creator, str(CREATE_PATH)) + avalon.api.register_plugin_path(LegacyCreator, str(CREATE_PATH)) lib.append_user_scripts() @@ -67,7 +68,7 @@ def uninstall(): pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) avalon.api.deregister_plugin_path(avalon.api.Loader, str(LOAD_PATH)) - avalon.api.deregister_plugin_path(avalon.api.Creator, str(CREATE_PATH)) + avalon.api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH)) if not IS_HEADLESS: ops.unregister() diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 8c9ab9a27f..20d1e4c8db 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional import bpy import avalon.api -from openpype.api import PypeCreatorMixin +from openpype.pipeline import LegacyCreator from .pipeline import AVALON_CONTAINERS from .ops import ( MainThreadItem, @@ -129,7 +129,7 @@ def deselect_all(): bpy.context.view_layer.objects.active = active -class Creator(PypeCreatorMixin, avalon.api.Creator): +class Creator(LegacyCreator): """Base class for Creator plug-ins.""" defaults = ['Main'] diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index 8029c38b4a..7f8ae610c6 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -8,6 +8,7 @@ import bpy from avalon import api from openpype import lib +from openpype.pipeline import legacy_create from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import ( AVALON_CONTAINERS, @@ -159,7 +160,7 @@ class BlendLayoutLoader(plugin.AssetLoader): raise ValueError("Creator plugin \"CreateAnimation\" was " "not found.") - api.create( + legacy_create( creator_plugin, name=local_obj.name.split(':')[-1] + "_animation", asset=asset, diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 0a5bdeecaa..91817deb60 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -8,7 +8,7 @@ from typing import Dict, Optional import bpy from avalon import api -from openpype import lib +from openpype.pipeline import legacy_create from openpype.hosts.blender.api.pipeline import ( AVALON_INSTANCES, AVALON_CONTAINERS, @@ -118,7 +118,7 @@ class JsonLayoutLoader(plugin.AssetLoader): # raise ValueError("Creator plugin \"CreateCamera\" was " # "not found.") - # api.create( + # legacy_create( # creator_plugin, # name="camera", # # name=f"{unique_number}_{subset}_animation", diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index eb6d273a51..eacabd3447 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -9,6 +9,7 @@ import bpy from avalon import api from avalon.blender import lib as avalon_lib from openpype import lib +from openpype.pipeline import legacy_create from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import ( AVALON_CONTAINERS, @@ -248,7 +249,7 @@ class BlendRigLoader(plugin.AssetLoader): animation_asset = options.get('animation_asset') - api.create( + legacy_create( creator_plugin, name=namespace + "_animation", # name=f"{unique_number}_{subset}_animation", diff --git a/openpype/hosts/flame/api/pipeline.py b/openpype/hosts/flame/api/pipeline.py index af071439ef..f802cf160b 100644 --- a/openpype/hosts/flame/api/pipeline.py +++ b/openpype/hosts/flame/api/pipeline.py @@ -7,6 +7,7 @@ from avalon import api as avalon from avalon.pipeline import AVALON_CONTAINER_ID from pyblish import api as pyblish from openpype.api import Logger +from openpype.pipeline import LegacyCreator from .lib import ( set_segment_data_marker, set_publish_attribute, @@ -33,7 +34,7 @@ def install(): pyblish.register_host("flame") pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) log.info("OpenPype Flame plug-ins registred ...") @@ -48,7 +49,7 @@ def uninstall(): log.info("Deregistering Flame plug-ins..") pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) # register callback for switching publishable diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index ec49db1601..92296752de 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -7,6 +7,7 @@ from xml.etree import ElementTree as ET import six from Qt import QtWidgets, QtCore import openpype.api as openpype +from openpype.pipeline import LegacyCreator from openpype import style import avalon.api as avalon from . import ( @@ -299,7 +300,7 @@ class Spacer(QtWidgets.QWidget): self.setLayout(layout) -class Creator(openpype.Creator): +class Creator(LegacyCreator): """Creator class wrapper """ clip_color = constants.COLOR_MAP["purple"] diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 64dda0bc8a..5ac56fcbed 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -11,6 +11,7 @@ import avalon.api from avalon.pipeline import AVALON_CONTAINER_ID from openpype.api import Logger +from openpype.pipeline import LegacyCreator import openpype.hosts.fusion log = Logger().get_logger(__name__) @@ -63,7 +64,7 @@ def install(): log.info("Registering Fusion plug-ins..") avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) pyblish.api.register_callback( @@ -87,7 +88,7 @@ def uninstall(): log.info("Deregistering Fusion plug-ins..") avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.deregister_plugin_path( avalon.api.InventoryAction, INVENTORY_PATH ) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 04717f4746..ff8bdb21ef 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,13 +1,13 @@ import os -import openpype.api +from openpype.pipeline import create from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk ) -class CreateOpenEXRSaver(openpype.api.Creator): +class CreateOpenEXRSaver(create.LegacyCreator): name = "openexrDefault" label = "Create OpenEXR Saver" diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 17d2870876..6d0f5e9416 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -9,6 +9,7 @@ import avalon.api from avalon.pipeline import AVALON_CONTAINER_ID from openpype import lib +from openpype.pipeline import LegacyCreator import openpype.hosts.harmony import openpype.hosts.harmony.api as harmony @@ -179,7 +180,7 @@ def install(): pyblish.api.register_host("harmony") pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) log.info(PUBLISH_PATH) # Register callbacks. @@ -193,7 +194,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def on_pyblish_instance_toggled(instance, old_value, new_value): diff --git a/openpype/hosts/harmony/api/plugin.py b/openpype/hosts/harmony/api/plugin.py index d6d61a547a..c55d200d30 100644 --- a/openpype/hosts/harmony/api/plugin.py +++ b/openpype/hosts/harmony/api/plugin.py @@ -1,9 +1,8 @@ -import avalon.api -from openpype.api import PypeCreatorMixin +from openpype.pipeline import LegacyCreator import openpype.hosts.harmony.api as harmony -class Creator(PypeCreatorMixin, avalon.api.Creator): +class Creator(LegacyCreator): """Creator plugin to create instances in Harmony. By default a Composite node is created to support any number of nodes in diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index cbcaf23994..5cb23ea355 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -9,6 +9,7 @@ from avalon import api as avalon from avalon import schema from pyblish import api as pyblish from openpype.api import Logger +from openpype.pipeline import LegacyCreator from openpype.tools.utils import host_tools from . import lib, menu, events @@ -45,7 +46,7 @@ def install(): pyblish.register_host("hiero") pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) # register callback for switching publishable @@ -67,7 +68,7 @@ def uninstall(): pyblish.deregister_host("hiero") pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) # register callback for switching publishable pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 3506af2d6a..3963985f0c 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -1,12 +1,15 @@ -import re import os +import re +from copy import deepcopy + import hiero + from Qt import QtWidgets, QtCore from avalon.vendor import qargparse import avalon.api as avalon import openpype.api as openpype +from openpype.pipeline import LegacyCreator from . import lib -from copy import deepcopy log = openpype.Logger().get_logger(__name__) @@ -589,7 +592,7 @@ class ClipLoader: return track_item -class Creator(openpype.Creator): +class Creator(LegacyCreator): """Creator class wrapper """ clip_color = "Purple" diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 1c08e72d65..21027dad2e 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -11,6 +11,7 @@ import avalon.api from avalon.pipeline import AVALON_CONTAINER_ID from avalon.lib import find_submodule +from openpype.pipeline import LegacyCreator import openpype.hosts.houdini from openpype.hosts.houdini.api import lib @@ -48,7 +49,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) log.info("Installing callbacks ... ") # avalon.on("init", on_init) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 4967d01d43..2bbb65aa05 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -2,11 +2,12 @@ """Houdini specific Avalon/Pyblish plugin definitions.""" import sys import six -import avalon.api -from avalon.api import CreatorError import hou -from openpype.api import PypeCreatorMixin +from openpype.pipeline import ( + CreatorError, + LegacyCreator +) from .lib import imprint @@ -14,7 +15,7 @@ class OpenPypeCreatorError(CreatorError): pass -class Creator(PypeCreatorMixin, avalon.api.Creator): +class Creator(LegacyCreator): """Creator plugin to create instances in Houdini To support the wide range of node types for render output (Alembic, VDB, diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 1b3bb9feb3..8c3669c5d1 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -2,7 +2,6 @@ import os import sys import errno import logging -import contextlib from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om @@ -17,6 +16,7 @@ import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import any_outdated from openpype.lib.path_tools import HostDirmap +from openpype.pipeline import LegacyCreator from openpype.hosts.maya.lib import copy_workspace_mel from . import menu, lib @@ -50,7 +50,7 @@ def install(): pyblish.api.register_host("maya") avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) log.info(PUBLISH_PATH) @@ -176,7 +176,7 @@ def uninstall(): pyblish.api.deregister_host("maya") avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.deregister_plugin_path( avalon.api.InventoryAction, INVENTORY_PATH ) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index bdb8fcf13a..5e52985fec 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -4,7 +4,7 @@ from maya import cmds from avalon import api from avalon.vendor import qargparse -from openpype.api import PypeCreatorMixin +from openpype.pipeline import LegacyCreator from .pipeline import containerise from . import lib @@ -77,7 +77,7 @@ def get_reference_node_parents(ref): return parents -class Creator(PypeCreatorMixin, api.Creator): +class Creator(LegacyCreator): defaults = ['Main'] def process(self): diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 743ec26778..9002ae3876 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -19,9 +19,9 @@ from openpype.api import ( get_project_settings, get_asset) from openpype.modules import ModulesManager +from openpype.pipeline import CreatorError from avalon.api import Session -from avalon.api import CreatorError class CreateRender(plugin.Creator): diff --git a/openpype/hosts/maya/plugins/create/create_vrayscene.py b/openpype/hosts/maya/plugins/create/create_vrayscene.py index f2096d902e..fa9c59e016 100644 --- a/openpype/hosts/maya/plugins/create/create_vrayscene.py +++ b/openpype/hosts/maya/plugins/create/create_vrayscene.py @@ -19,10 +19,10 @@ from openpype.api import ( get_project_settings ) +from openpype.pipeline import CreatorError from openpype.modules import ModulesManager from avalon.api import Session -from avalon.api import CreatorError class CreateVRayScene(plugin.Creator): diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 0565b0b95c..25db5fb1e5 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -3,6 +3,7 @@ from maya import cmds from avalon import api from openpype.api import get_project_settings from openpype.lib import get_creator_by_name +from openpype.pipeline import legacy_create import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import maintained_selection @@ -151,7 +152,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): creator_plugin = get_creator_by_name(self.animation_creator_name) with maintained_selection(): cmds.select([output, controls] + roots, noExpand=True) - api.create( + legacy_create( creator_plugin, name=namespace, asset=asset, diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 8c6c9ca55b..d98a951491 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -14,6 +14,7 @@ from openpype.api import ( BuildWorkfile, get_current_project_settings ) +from openpype.pipeline import LegacyCreator from openpype.tools.utils import host_tools from .command import viewer_update_and_undo_stop @@ -98,7 +99,7 @@ def install(): log.info("Registering Nuke plug-ins..") pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) # Register Avalon event for workfiles loading. @@ -124,7 +125,7 @@ def uninstall(): pyblish.deregister_host("nuke") pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) pyblish.api.deregister_callback( "instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 11e30d9fcd..ff186cd685 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -6,10 +6,8 @@ import nuke import avalon.api -from openpype.api import ( - get_current_project_settings, - PypeCreatorMixin -) +from openpype.api import get_current_project_settings +from openpype.pipeline import LegacyCreator from .lib import ( Knobby, check_subsetname_exists, @@ -20,7 +18,7 @@ from .lib import ( ) -class OpenPypeCreator(PypeCreatorMixin, avalon.api.Creator): +class OpenPypeCreator(LegacyCreator): """Pype Nuke Creator class wrapper""" node_color = "0xdfea5dff" diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index 4cc2aa2c78..17ea957066 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -16,7 +16,6 @@ from .pipeline import ( ) from .plugin import ( PhotoshopLoader, - Creator, get_unique_layer_name ) from .workio import ( @@ -42,11 +41,11 @@ __all__ = [ "list_instances", "remove_instance", "install", + "uninstall", "containerise", # Plugin "PhotoshopLoader", - "Creator", "get_unique_layer_name", # workfiles diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 25983f2471..662e9dbebc 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -1,5 +1,4 @@ import os -import sys from Qt import QtWidgets import pyblish.api @@ -7,6 +6,7 @@ import avalon.api from avalon import pipeline, io from openpype.api import Logger +from openpype.pipeline import LegacyCreator import openpype.hosts.photoshop from . import lib @@ -68,7 +68,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( @@ -81,7 +81,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def ls(): diff --git a/openpype/hosts/photoshop/api/plugin.py b/openpype/hosts/photoshop/api/plugin.py index e0db67de2c..c577c67d82 100644 --- a/openpype/hosts/photoshop/api/plugin.py +++ b/openpype/hosts/photoshop/api/plugin.py @@ -33,37 +33,3 @@ class PhotoshopLoader(avalon.api.Loader): @staticmethod def get_stub(): return stub() - - -class Creator(avalon.api.Creator): - """Creator plugin to create instances in Photoshop - - A LayerSet is created to support any number of layers in an instance. If - the selection is used, these layers will be added to the LayerSet. - """ - - def process(self): - # Photoshop can have multiple LayerSets with the same name, which does - # not work with Avalon. - msg = "Instance with name \"{}\" already exists.".format(self.name) - stub = lib.stub() # only after Photoshop is up - for layer in stub.get_layers(): - if self.name.lower() == layer.Name.lower(): - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Warning) - msg.setText(msg) - msg.exec_() - return False - - # Store selection because adding a group will change selection. - with lib.maintained_selection(): - - # Add selection to group. - if (self.options or {}).get("useSelection"): - group = stub.group_selected_layers(self.name) - else: - group = stub.create_group(self.name) - - stub.imprint(group, self.data) - - return group diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 344a53f47e..a001b5f171 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -1,9 +1,9 @@ from Qt import QtWidgets -import openpype.api +from openpype.pipeline import create from openpype.hosts.photoshop import api as photoshop -class CreateImage(openpype.api.Creator): +class CreateImage(create.LegacyCreator): """Image folder for publish.""" name = "imageDefault" diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 2dc5136c8a..c82545268b 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -9,6 +9,7 @@ from avalon import schema from avalon.pipeline import AVALON_CONTAINER_ID from pyblish import api as pyblish from openpype.api import Logger +from openpype.pipeline import LegacyCreator from . import lib from . import PLUGINS_DIR from openpype.tools.utils import host_tools @@ -42,7 +43,7 @@ def install(): log.info("Registering DaVinci Resovle plug-ins..") avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) # register callback for switching publishable @@ -67,7 +68,7 @@ def uninstall(): log.info("Deregistering DaVinci Resovle plug-ins..") avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) # register callback for switching publishable diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 8612cf82ec..b6791f7225 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -2,6 +2,7 @@ import re import uuid from avalon import api import openpype.api as pype +from openpype.pipeline import LegacyCreator from openpype.hosts import resolve from avalon.vendor import qargparse from . import lib @@ -493,7 +494,7 @@ class TimelineItemLoader(api.Loader): pass -class Creator(pype.PypeCreatorMixin, api.Creator): +class Creator(LegacyCreator): """Creator class wrapper """ marker_color = "Purple" diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 74eb41892c..f4599047b4 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -14,6 +14,7 @@ from avalon.pipeline import AVALON_CONTAINER_ID from openpype.hosts import tvpaint from openpype.api import get_current_project_settings +from openpype.pipeline import LegacyCreator from .lib import ( execute_george, @@ -76,7 +77,7 @@ def install(): pyblish.api.register_host("tvpaint") pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) registered_callbacks = ( pyblish.api.registered_callbacks().get("instanceToggled") or [] @@ -98,7 +99,7 @@ def uninstall(): pyblish.api.deregister_host("tvpaint") pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def containerise( diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index af80c9eae2..8510794f06 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -3,14 +3,14 @@ import uuid import avalon.api -from openpype.api import PypeCreatorMixin +from openpype.pipeline import LegacyCreator from openpype.hosts.tvpaint.api import ( pipeline, lib ) -class Creator(PypeCreatorMixin, avalon.api.Creator): +class Creator(LegacyCreator): def __init__(self, *args, **kwargs): super(Creator, self).__init__(*args, **kwargs) # Add unified identifier created with `uuid` module diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 40a7d15990..c1af9632b1 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -1,5 +1,4 @@ -from avalon.api import CreatorError - +from openpype.pipeline import CreatorError from openpype.lib import prepare_template_data from openpype.hosts.tvpaint.api import ( plugin, diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index af962052fc..a7f717ccec 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -1,4 +1,4 @@ -from avalon.api import CreatorError +from openpype.pipeline import CreatorError from openpype.lib import prepare_template_data from openpype.hosts.tvpaint.api import ( plugin, diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index ad64d56e9e..8ab19bd697 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -7,6 +7,7 @@ import pyblish.api from avalon.pipeline import AVALON_CONTAINER_ID from avalon import api +from openpype.pipeline import LegacyCreator from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -44,7 +45,7 @@ def install(): logger.info("installing OpenPype for Unreal") pyblish.api.register_plugin_path(str(PUBLISH_PATH)) api.register_plugin_path(api.Loader, str(LOAD_PATH)) - api.register_plugin_path(api.Creator, str(CREATE_PATH)) + api.register_plugin_path(LegacyCreator, str(CREATE_PATH)) _register_callbacks() _register_events() @@ -53,7 +54,7 @@ def uninstall(): """Uninstall Unreal configuration for Avalon.""" pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) api.deregister_plugin_path(api.Loader, str(LOAD_PATH)) - api.deregister_plugin_path(api.Creator, str(CREATE_PATH)) + api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH)) def _register_callbacks(): @@ -70,7 +71,7 @@ def _register_events(): pass -class Creator(api.Creator): +class Creator(LegacyCreator): hosts = ["unreal"] asset_types = [] diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 2327fc09c8..dd2e7750f0 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from abc import ABC -import openpype.api +from openpype.pipeline import LegacyCreator import avalon.api -class Creator(openpype.api.Creator): +class Creator(LegacyCreator): """This serves as skeleton for future OpenPype specific functionality""" defaults = ['Main'] diff --git a/openpype/hosts/webpublisher/api/__init__.py b/openpype/hosts/webpublisher/api/__init__.py index e40d46d662..6ce8a58fc2 100644 --- a/openpype/hosts/webpublisher/api/__init__.py +++ b/openpype/hosts/webpublisher/api/__init__.py @@ -5,6 +5,7 @@ from avalon import api as avalon from avalon import io from pyblish import api as pyblish import openpype.hosts.webpublisher +from openpype.pipeline import LegacyCreator log = logging.getLogger("openpype.hosts.webpublisher") @@ -25,7 +26,7 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(LegacyCreator, CREATE_PATH) log.info(PUBLISH_PATH) io.install() @@ -35,7 +36,7 @@ def install(): def uninstall(): pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) # to have required methods for interface diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 183aad939a..19765a6f4a 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -293,7 +293,7 @@ def set_plugin_attributes_from_settings( plugin_type = None if superclass.__name__.split(".")[-1] in ("Loader", "SubsetLoader"): plugin_type = "load" - elif superclass.__name__.split(".")[-1] == "Creator": + elif superclass.__name__.split(".")[-1] in ("Creator", "LegacyCreator"): plugin_type = "create" if not host_name or not project_name or plugin_type is None: diff --git a/openpype/plugin.py b/openpype/plugin.py index 45c9a08209..3569936dac 100644 --- a/openpype/plugin.py +++ b/openpype/plugin.py @@ -3,79 +3,12 @@ import os import pyblish.api import avalon.api -from openpype.lib import get_subset_name - ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 -class PypeCreatorMixin: - """Helper to override avalon's default class methods. - - Mixin class must be used as first in inheritance order to override methods. - """ - dynamic_subset_keys = [] - - @classmethod - def get_dynamic_data( - cls, variant, task_name, asset_id, project_name, host_name - ): - """Return dynamic data for current Creator plugin. - - By default return keys from `dynamic_subset_keys` attribute as mapping - to keep formatted template unchanged. - - ``` - dynamic_subset_keys = ["my_key"] - --- - output = { - "my_key": "{my_key}" - } - ``` - - Dynamic keys may override default Creator keys (family, task, asset, - ...) but do it wisely if you need. - - All of keys will be converted into 3 variants unchanged, capitalized - and all upper letters. Because of that are all keys lowered. - - This method can be modified to prefill some values just keep in mind it - is class method. - - Returns: - dict: Fill data for subset name template. - """ - dynamic_data = {} - for key in cls.dynamic_subset_keys: - key = key.lower() - dynamic_data[key] = "{" + key + "}" - return dynamic_data - - @classmethod - def get_subset_name( - cls, variant, task_name, asset_id, project_name, host_name=None - ): - dynamic_data = cls.get_dynamic_data( - variant, task_name, asset_id, project_name, host_name - ) - - return get_subset_name( - cls.family, - variant, - task_name, - asset_id, - project_name, - host_name, - dynamic_data=dynamic_data - ) - - -class Creator(PypeCreatorMixin, avalon.api.Creator): - pass - - class ContextPlugin(pyblish.api.ContextPlugin): def process(cls, *args, **kwargs): super(ContextPlugin, cls).process(cls, *args, **kwargs) diff --git a/openpype/tests/test_avalon_plugin_presets.py b/openpype/tests/test_avalon_plugin_presets.py index ec21385d23..f1b1a94713 100644 --- a/openpype/tests/test_avalon_plugin_presets.py +++ b/openpype/tests/test_avalon_plugin_presets.py @@ -1,8 +1,9 @@ import avalon.api as api import openpype +from openpype.pipeline import LegacyCreator -class MyTestCreator(api.Creator): +class MyTestCreator(LegacyCreator): my_test_property = "A" @@ -26,8 +27,8 @@ def test_avalon_plugin_presets(monkeypatch, printer): openpype.install() api.register_host(Test()) - api.register_plugin(api.Creator, MyTestCreator) - plugins = api.discover(api.Creator) + api.register_plugin(LegacyCreator, MyTestCreator) + plugins = api.discover(LegacyCreator) printer("Test if we got our test plugin") assert MyTestCreator in plugins for p in plugins: diff --git a/openpype/tools/creator/model.py b/openpype/tools/creator/model.py index 6907e8f0aa..ef61c6e0f0 100644 --- a/openpype/tools/creator/model.py +++ b/openpype/tools/creator/model.py @@ -2,6 +2,7 @@ import uuid from Qt import QtGui, QtCore from avalon import api +from openpype.pipeline import LegacyCreator from . constants import ( FAMILY_ROLE, @@ -21,7 +22,7 @@ class CreatorsModel(QtGui.QStandardItemModel): self._creators_by_id = {} items = [] - creators = api.discover(api.Creator) + creators = api.discover(LegacyCreator) for creator in creators: item_id = str(uuid.uuid4()) self._creators_by_id[item_id] = creator diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index f1d0849dfe..51cc66e715 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -9,7 +9,12 @@ from avalon import api, io from openpype import style from openpype.api import get_current_project_settings from openpype.tools.utils.lib import qt_app_context -from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS +from openpype.pipeline.create import ( + SUBSET_NAME_ALLOWED_SYMBOLS, + legacy_create, + CreatorError, + LegacyCreator, +) from .model import CreatorsModel from .widgets import ( @@ -422,7 +427,7 @@ class CreatorWindow(QtWidgets.QDialog): error_info = None try: - api.create( + legacy_create( creator_plugin, subset_name, asset_name, @@ -430,7 +435,7 @@ class CreatorWindow(QtWidgets.QDialog): data={"variant": variant} ) - except api.CreatorError as exc: + except CreatorError as exc: self.echo("Creator error: {}".format(str(exc))) error_info = (str(exc), None) @@ -486,7 +491,7 @@ def show(debug=False, parent=None): if debug: from avalon import mock for creator in mock.creators: - api.register_plugin(api.Creator, creator) + api.register_plugin(LegacyCreator, creator) import traceback sys.excepthook = lambda typ, val, tb: traceback.print_last() diff --git a/openpype/tools/standalonepublish/widgets/widget_family.py b/openpype/tools/standalonepublish/widgets/widget_family.py index ae44899a89..08cd45bbf2 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family.py +++ b/openpype/tools/standalonepublish/widgets/widget_family.py @@ -1,14 +1,11 @@ -import os import re from Qt import QtWidgets, QtCore from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole from . import FamilyDescriptionWidget -from openpype.api import ( - get_project_settings, - Creator -) +from openpype.api import get_project_settings +from openpype.pipeline import LegacyCreator from openpype.lib import TaskNotSetError from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS @@ -390,7 +387,7 @@ class FamilyWidget(QtWidgets.QWidget): sp_settings = settings.get('standalonepublisher', {}) for key, creator_data in sp_settings.get("create", {}).items(): - creator = type(key, (Creator, ), creator_data) + creator = type(key, (LegacyCreator, ), creator_data) label = creator.label or creator.family item = QtWidgets.QListWidgetItem(label) From ee71d3b6580f363d95404b13283339ea055a183d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 10:13:51 +0100 Subject: [PATCH 12/29] fix getattr clalback on dynamic module --- openpype/modules/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index c7078475df..175957ae39 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -61,6 +61,7 @@ class _ModuleClass(object): def __init__(self, name): # Call setattr on super class super(_ModuleClass, self).__setattr__("name", name) + super(_ModuleClass, self).__setattr__("__name__", name) # Where modules and interfaces are stored super(_ModuleClass, self).__setattr__("__attributes__", dict()) @@ -72,7 +73,7 @@ class _ModuleClass(object): if attr_name not in self.__attributes__: if attr_name in ("__path__", "__file__"): return None - raise ImportError("No module named {}.{}".format( + raise AttributeError("'{}' has not attribute '{}'".format( self.name, attr_name )) return self.__attributes__[attr_name] From aef78c3c7580c978873c3e89ea34e1a00a2a4b92 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 11:34:02 +0100 Subject: [PATCH 13/29] use new version of error dialog and pass parent to a dialog --- .../tools/publisher/widgets/create_dialog.py | 124 +++++++++--------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index c5b77eca8b..5ebcd7291d 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -14,6 +14,8 @@ from openpype.pipeline.create import ( SUBSET_NAME_ALLOWED_SYMBOLS ) +from openpype.tools.utils import ErrorMessageBox + from .widgets import IconValuePixmapLabel from .assets_widget import CreateDialogAssetsWidget from .tasks_widget import CreateDialogTasksWidget @@ -27,7 +29,7 @@ from ..constants import ( SEPARATORS = ("---separator---", "---") -class CreateErrorMessageBox(QtWidgets.QDialog): +class CreateErrorMessageBox(ErrorMessageBox): def __init__( self, creator_label, @@ -35,24 +37,38 @@ class CreateErrorMessageBox(QtWidgets.QDialog): asset_name, exc_msg, formatted_traceback, - parent=None + parent ): - super(CreateErrorMessageBox, self).__init__(parent) - self.setWindowTitle("Creation failed") - self.setFocusPolicy(QtCore.Qt.StrongFocus) - if not parent: - self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint - ) + self._creator_label = creator_label + self._subset_name = subset_name + self._asset_name = asset_name + self._exc_msg = exc_msg + self._formatted_traceback = formatted_traceback + super(CreateErrorMessageBox, self).__init__("Creation failed", parent) - body_layout = QtWidgets.QVBoxLayout(self) - - main_label = ( + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( "Failed to create" ) - main_label_widget = QtWidgets.QLabel(main_label, self) - body_layout.addWidget(main_label_widget) + return label_widget + def _get_report_data(self): + report_message = ( + "{creator}: Failed to create Subset: \"{subset}\"" + " in Asset: \"{asset}\"" + "\n\nError: {message}" + ).format( + creator=self._creator_label, + subset=self._subset_name, + asset=self._asset_name, + message=self._exc_msg, + ) + if self._formatted_traceback: + report_message += "\n\n{}".format(self._formatted_traceback) + return [report_message] + + def _create_content(self, content_layout): item_name_template = ( "Creator: {}
" "Subset: {}
" @@ -61,48 +77,29 @@ class CreateErrorMessageBox(QtWidgets.QDialog): exc_msg_template = "{}" line = self._create_line() - body_layout.addWidget(line) + content_layout.addWidget(line) - item_name = item_name_template.format( - creator_label, subset_name, asset_name - ) - item_name_widget = QtWidgets.QLabel( - item_name.replace("\n", "
"), self - ) - body_layout.addWidget(item_name_widget) - - exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
")) - message_label_widget = QtWidgets.QLabel(exc_msg, self) - body_layout.addWidget(message_label_widget) - - if formatted_traceback: - tb_widget = QtWidgets.QLabel( - formatted_traceback.replace("\n", "
"), self + item_name_widget = QtWidgets.QLabel(self) + item_name_widget.setText( + item_name_template.format( + self._creator_label, self._subset_name, self._asset_name ) - tb_widget.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - body_layout.addWidget(tb_widget) - - footer_widget = QtWidgets.QWidget(self) - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - button_box = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical) - button_box.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Ok ) - button_box.accepted.connect(self._on_accept) - footer_layout.addWidget(button_box, alignment=QtCore.Qt.AlignRight) - body_layout.addWidget(footer_widget) + content_layout.addWidget(item_name_widget) - def _on_accept(self): - self.close() + message_label_widget = QtWidgets.QLabel(self) + message_label_widget.setText( + exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) + ) + content_layout.addWidget(message_label_widget) - def _create_line(self): - line = QtWidgets.QFrame(self) - line.setFixedHeight(2) - line.setFrameShape(QtWidgets.QFrame.HLine) - line.setFrameShadow(QtWidgets.QFrame.Sunken) - return line + if self._formatted_traceback: + line_widget = self._create_line() + tb_widget = self._create_traceback_widget( + self._formatted_traceback + ) + content_layout.addWidget(line_widget) + content_layout.addWidget(tb_widget) # TODO add creator identifier/label to details @@ -201,7 +198,7 @@ class CreateDialog(QtWidgets.QDialog): self._prereq_available = False - self.message_dialog = None + self._message_dialog = None name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) self._name_pattern = name_pattern @@ -694,14 +691,18 @@ class CreateDialog(QtWidgets.QDialog): "family": family } - error_info = None + error_msg = None + formatted_traceback = None try: self.controller.create( - creator_identifier, subset_name, instance_data, pre_create_data + creator_identifier, + subset_name, + instance_data, + pre_create_data ) except CreatorError as exc: - error_info = (str(exc), None) + error_msg = str(exc) # Use bare except because some hosts raise their exceptions that # do not inherit from python's `BaseException` @@ -710,12 +711,17 @@ class CreateDialog(QtWidgets.QDialog): formatted_traceback = "".join(traceback.format_exception( exc_type, exc_value, exc_traceback )) - error_info = (str(exc_value), formatted_traceback) + error_msg = str(exc_value) - if error_info: + if error_msg is not None: box = CreateErrorMessageBox( - creator_label, subset_name, asset_name, *error_info + creator_label, + subset_name, + asset_name, + error_msg, + formatted_traceback, + parent=self ) box.show() # Store dialog so is not garbage collected before is shown - self.message_dialog = box + self._message_dialog = box From 65cd0c55a839b03fcc471e1215b2dcf3f46d974e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 12:08:11 +0100 Subject: [PATCH 14/29] added check of subclasses in patched discover --- openpype/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/__init__.py b/openpype/__init__.py index c41afaa47d..942112835b 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -59,10 +59,15 @@ def patched_discover(superclass): """ # run original discover and get plugins plugins = _original_discover(superclass) + filtered_plugins = [ + plugin + for plugin in plugins + if issubclass(plugin, superclass) + ] - set_plugin_attributes_from_settings(plugins, superclass) + set_plugin_attributes_from_settings(filtered_plugins, superclass) - return plugins + return filtered_plugins @import_wrapper From 6d2ebaa68e3ec4d448845d89738465edc671a255 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Mar 2022 12:11:27 +0100 Subject: [PATCH 15/29] remove unused code --- openpype/hosts/blender/plugins/load/load_layout_json.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 91817deb60..5b5f9ab83d 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -8,7 +8,6 @@ from typing import Dict, Optional import bpy from avalon import api -from openpype.pipeline import legacy_create from openpype.hosts.blender.api.pipeline import ( AVALON_INSTANCES, AVALON_CONTAINERS, From f515f360dc204d122a7288306a1bd47289959fdc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 14:34:18 +0100 Subject: [PATCH 16/29] Choose project widget is more clear --- openpype/style/style.css | 6 ++++ openpype/tools/traypublisher/window.py | 38 +++++++++++++++++--------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index ba40b780ab..5586cf766d 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1266,6 +1266,12 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { font-size: 15pt; font-weight: 750; } +#ChooseProjectFrame { + border-radius: 10px; +} +#ChooseProjectView { + background: transparent; +} /* Globally used names */ #Separator { diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 53f8ca450a..4c35d84f98 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -28,38 +28,50 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): super(StandaloneOverlayWidget, self).__init__(publisher_window) self.setObjectName("OverlayFrame") + middle_frame = QtWidgets.QFrame(self) + middle_frame.setObjectName("ChooseProjectFrame") + + content_widget = QtWidgets.QWidget(middle_frame) + # Create db connection for projects model dbcon = AvalonMongoDB() dbcon.install() - header_label = QtWidgets.QLabel("Choose project", self) + header_label = QtWidgets.QLabel("Choose project", content_widget) header_label.setObjectName("ChooseProjectLabel") # Create project models and view projects_model = ProjectModel(dbcon) projects_proxy = ProjectSortFilterProxy() projects_proxy.setSourceModel(projects_model) - projects_view = QtWidgets.QListView(self) + projects_view = QtWidgets.QListView(content_widget) + projects_view.setObjectName("ChooseProjectView") projects_view.setModel(projects_proxy) projects_view.setEditTriggers( QtWidgets.QAbstractItemView.NoEditTriggers ) - confirm_btn = QtWidgets.QPushButton("Choose", self) + confirm_btn = QtWidgets.QPushButton("Confirm", content_widget) btns_layout = QtWidgets.QHBoxLayout() btns_layout.addStretch(1) btns_layout.addWidget(confirm_btn, 0) - layout = QtWidgets.QGridLayout(self) - layout.addWidget(header_label, 0, 1, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(projects_view, 1, 1) - layout.addLayout(btns_layout, 2, 1) - layout.setColumnStretch(0, 1) - layout.setColumnStretch(1, 0) - layout.setColumnStretch(2, 1) - layout.setRowStretch(0, 0) - layout.setRowStretch(1, 1) - layout.setRowStretch(2, 0) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(20) + content_layout.addWidget(header_label, 0) + content_layout.addWidget(projects_view, 1) + content_layout.addLayout(btns_layout, 0) + + middle_layout = QtWidgets.QHBoxLayout(middle_frame) + middle_layout.setContentsMargins(30, 30, 10, 10) + middle_layout.addWidget(content_widget) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.addStretch(1) + main_layout.addWidget(middle_frame, 3) + main_layout.addStretch(1) projects_view.doubleClicked.connect(self._on_double_click) confirm_btn.clicked.connect(self._on_confirm_click) From 9280eacf40d821bad8fa85c8db78ac551728b957 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 14:38:22 +0100 Subject: [PATCH 17/29] missing task does not make invalid asset --- openpype/pipeline/create/context.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 706279fd72..c2757a4502 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1005,12 +1005,14 @@ class CreateContext: if not instances: return - task_names_by_asset_name = collections.defaultdict(set) + task_names_by_asset_name = {} for instance in instances: task_name = instance.get("task") asset_name = instance.get("asset") - if asset_name and task_name: - task_names_by_asset_name[asset_name].add(task_name) + if asset_name: + task_names_by_asset_name[asset_name] = set() + if task_name: + task_names_by_asset_name[asset_name].add(task_name) asset_names = [ asset_name From 20b3b38fb8ed4ecedd0dca398fae0a934f16f1b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 16:18:02 +0100 Subject: [PATCH 18/29] change ratio --- openpype/tools/traypublisher/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 4c35d84f98..d0453c4f23 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -70,7 +70,7 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(10, 10, 10, 10) main_layout.addStretch(1) - main_layout.addWidget(middle_frame, 3) + main_layout.addWidget(middle_frame, 2) main_layout.addStretch(1) projects_view.doubleClicked.connect(self._on_double_click) From acb7546e3896c6ec66c9c80a9e6fb68fa2a6468f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 16:25:03 +0100 Subject: [PATCH 19/29] handle 'TaskNotSetError' in create dialog --- .../tools/publisher/widgets/create_dialog.py | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index c5b77eca8b..607060da7e 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -8,7 +8,7 @@ try: except Exception: commonmark = None from Qt import QtWidgets, QtCore, QtGui - +from openpype.lib import TaskNotSetError from openpype.pipeline.create import ( CreatorError, SUBSET_NAME_ALLOWED_SYMBOLS @@ -566,10 +566,9 @@ class CreateDialog(QtWidgets.QDialog): if variant_value is None: variant_value = self.variant_input.text() - match = self._compiled_name_pattern.match(variant_value) - valid = bool(match) - self.create_btn.setEnabled(valid) - if not valid: + self.create_btn.setEnabled(True) + if not self._compiled_name_pattern.match(variant_value): + self.create_btn.setEnabled(False) self._set_variant_state_property("invalid") self.subset_name_input.setText("< Invalid variant >") return @@ -579,9 +578,16 @@ class CreateDialog(QtWidgets.QDialog): asset_doc = copy.deepcopy(self._asset_doc) # Calculate subset name with Creator plugin - subset_name = self._selected_creator.get_subset_name( - variant_value, task_name, asset_doc, project_name - ) + try: + subset_name = self._selected_creator.get_subset_name( + variant_value, task_name, asset_doc, project_name + ) + except TaskNotSetError: + self.create_btn.setEnabled(False) + self._set_variant_state_property("invalid") + self.subset_name_input.setText("< Missing task >") + return + self.subset_name_input.setText(subset_name) self._validate_subset_name(subset_name, variant_value) From a0b2995f15c2f40b81b73d0f8356a75eaa81bc32 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 17:39:08 +0100 Subject: [PATCH 20/29] tasks model has ability to have empty task --- .../tools/publisher/widgets/tasks_widget.py | 19 +++++++--- openpype/tools/publisher/widgets/widgets.py | 35 +++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openpype/tools/publisher/widgets/tasks_widget.py b/openpype/tools/publisher/widgets/tasks_widget.py index a0b3a340ae..2d1cc017af 100644 --- a/openpype/tools/publisher/widgets/tasks_widget.py +++ b/openpype/tools/publisher/widgets/tasks_widget.py @@ -17,9 +17,10 @@ class TasksModel(QtGui.QStandardItemModel): controller (PublisherController): Controller which handles creation and publishing. """ - def __init__(self, controller): + def __init__(self, controller, allow_empty_task=False): super(TasksModel, self).__init__() + self._allow_empty_task = allow_empty_task self._controller = controller self._items_by_name = {} self._asset_names = [] @@ -70,8 +71,14 @@ class TasksModel(QtGui.QStandardItemModel): task_name (str): Name of task which should be available in asset's tasks. """ - task_names = self._task_names_by_asset_name.get(asset_name) - if task_names and task_name in task_names: + if asset_name not in self._task_names_by_asset_name: + return False + + if self._allow_empty_task and not task_name: + return True + + task_names = self._task_names_by_asset_name[asset_name] + if task_name in task_names: return True return False @@ -92,6 +99,8 @@ class TasksModel(QtGui.QStandardItemModel): new_task_names = self.get_intersection_of_tasks( task_names_by_asset_name ) + if self._allow_empty_task: + new_task_names.add("") old_task_names = set(self._items_by_name.keys()) if new_task_names == old_task_names: return @@ -111,7 +120,9 @@ class TasksModel(QtGui.QStandardItemModel): item.setData(task_name, TASK_NAME_ROLE) self._items_by_name[task_name] = item new_items.append(item) - root_item.appendRows(new_items) + + if new_items: + root_item.appendRows(new_items) def headerData(self, section, orientation, role=None): if role is None: diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index fb1f0e54aa..3f913d7e52 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -7,6 +7,7 @@ from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome +from openpype.lib import TaskNotSetError from openpype.widgets.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm @@ -490,13 +491,16 @@ class TasksCombobox(QtWidgets.QComboBox): delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(delegate) - model = TasksModel(controller) - self.setModel(model) + model = TasksModel(controller, True) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSourceModel(model) + self.setModel(proxy_model) self.currentIndexChanged.connect(self._on_index_change) self._delegate = delegate self._model = model + self._proxy_model = proxy_model self._origin_value = [] self._origin_selection = [] self._selected_items = [] @@ -596,6 +600,7 @@ class TasksCombobox(QtWidgets.QComboBox): self._ignore_index_change = True self._model.set_asset_names(asset_names) + self._proxy_model.invalidate() self._ignore_index_change = False @@ -1016,10 +1021,26 @@ class GlobalAttrsWidget(QtWidgets.QWidget): asset_doc = asset_docs_by_name[new_asset_name] - new_subset_name = instance.creator.get_subset_name( - new_variant_value, new_task_name, asset_doc, project_name - ) + try: + new_subset_name = instance.creator.get_subset_name( + new_variant_value, new_task_name, asset_doc, project_name + ) + except TaskNotSetError: + instance.set_task_invalid(True) + continue + subset_names.add(new_subset_name) + if variant_value is not None: + instance["variant"] = variant_value + + if asset_name is not None: + instance["asset"] = asset_name + instance.set_asset_invalid(False) + + if task_name is not None: + instance["task"] = task_name + instance.set_task_invalid(False) + instance["subset"] = new_subset_name self.subset_value_widget.set_value(subset_names) @@ -1098,7 +1119,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): variants.add(instance.get("variant") or self.unknown_value) families.add(instance.get("family") or self.unknown_value) asset_name = instance.get("asset") or self.unknown_value - task_name = instance.get("task") or self.unknown_value + task_name = instance.get("task") + if task_name is None: + task_name = self.unknown_value asset_names.add(asset_name) asset_task_combinations.append((asset_name, task_name)) subset_names.add(instance.get("subset") or self.unknown_value) From 8efbe92ccb3b90981e8a17ba1ef4c1a9d81e22b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 17:39:41 +0100 Subject: [PATCH 21/29] changed "Sumbit" to "Confirm" --- openpype/tools/publisher/widgets/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 3f913d7e52..a5528e52a6 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -937,7 +937,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): family_value_widget.set_value() subset_value_widget.set_value() - submit_btn = QtWidgets.QPushButton("Submit", self) + submit_btn = QtWidgets.QPushButton("Confirm", self) cancel_btn = QtWidgets.QPushButton("Cancel", self) submit_btn.setEnabled(False) cancel_btn.setEnabled(False) From db1f2ce8c415f2fdf99c201f90602e18f00e69be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 17:44:33 +0100 Subject: [PATCH 22/29] fix method name --- openpype/hosts/testhost/plugins/publish/collect_context.py | 2 +- openpype/hosts/testhost/plugins/publish/collect_instance_1.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/testhost/plugins/publish/collect_context.py b/openpype/hosts/testhost/plugins/publish/collect_context.py index bbb8477cdf..0ab98fb84b 100644 --- a/openpype/hosts/testhost/plugins/publish/collect_context.py +++ b/openpype/hosts/testhost/plugins/publish/collect_context.py @@ -19,7 +19,7 @@ class CollectContextDataTestHost( hosts = ["testhost"] @classmethod - def get_instance_attr_defs(cls): + def get_attribute_defs(cls): return [ attribute_definitions.BoolDef( "test_bool", diff --git a/openpype/hosts/testhost/plugins/publish/collect_instance_1.py b/openpype/hosts/testhost/plugins/publish/collect_instance_1.py index 979ab83f11..3c035eccb6 100644 --- a/openpype/hosts/testhost/plugins/publish/collect_instance_1.py +++ b/openpype/hosts/testhost/plugins/publish/collect_instance_1.py @@ -20,7 +20,7 @@ class CollectInstanceOneTestHost( hosts = ["testhost"] @classmethod - def get_instance_attr_defs(cls): + def get_attribute_defs(cls): return [ attribute_definitions.NumberDef( "version", From c1304f4e691fec99de2d639992dee04a5957bc7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 18:45:05 +0100 Subject: [PATCH 23/29] it is possible to catch invalid tasks on confirm submit --- openpype/tools/publisher/widgets/widgets.py | 65 ++++++++++++++++----- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index a5528e52a6..ace7b4e02d 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -472,6 +472,28 @@ class AssetsField(BaseClickableFrame): self.set_selected_items(self._origin_value) +class TasksComboboxProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(TasksComboboxProxy, self).__init__(*args, **kwargs) + self._filter_empty = False + + def set_filter_empty(self, filter_empty): + if self._filter_empty is filter_empty: + return + self._filter_empty = filter_empty + self.invalidate() + + def filterAcceptsRow(self, source_row, parent_index): + if self._filter_empty: + model = self.sourceModel() + source_index = model.index( + source_row, self.filterKeyColumn(), parent_index + ) + if not source_index.data(QtCore.Qt.DisplayRole): + return False + return True + + class TasksCombobox(QtWidgets.QComboBox): """Combobox to show tasks for selected instances. @@ -492,7 +514,7 @@ class TasksCombobox(QtWidgets.QComboBox): self.setItemDelegate(delegate) model = TasksModel(controller, True) - proxy_model = QtCore.QSortFilterProxyModel() + proxy_model = TasksComboboxProxy() proxy_model.setSourceModel(model) self.setModel(proxy_model) @@ -511,6 +533,14 @@ class TasksCombobox(QtWidgets.QComboBox): self._text = None + def set_invalid_empty_task(self, invalid=True): + self._proxy_model.set_filter_empty(invalid) + if invalid: + self._set_is_valid(False) + self.set_text("< One or more subsets require Task selected >") + else: + self.set_text(None) + def set_multiselection_text(self, text): """Change text shown when multiple different tasks are in context.""" self._multiselection_text = text @@ -600,7 +630,8 @@ class TasksCombobox(QtWidgets.QComboBox): self._ignore_index_change = True self._model.set_asset_names(asset_names) - self._proxy_model.invalidate() + self._proxy_model.set_filter_empty(False) + self._proxy_model.sort(0) self._ignore_index_change = False @@ -646,6 +677,9 @@ class TasksCombobox(QtWidgets.QComboBox): asset_task_combinations (list): List of tuples. Each item in the list contain asset name and task name. """ + self._proxy_model.set_filter_empty(False) + self._proxy_model.sort(0) + if asset_task_combinations is None: asset_task_combinations = [] @@ -1003,21 +1037,19 @@ class GlobalAttrsWidget(QtWidgets.QWidget): project_name = self.controller.project_name subset_names = set() + invalid_tasks = False for instance in self._current_instances: - if variant_value is not None: - instance["variant"] = variant_value - - if asset_name is not None: - instance["asset"] = asset_name - instance.set_asset_invalid(False) - - if task_name is not None: - instance["task"] = task_name - instance.set_task_invalid(False) - new_variant_value = instance.get("variant") new_asset_name = instance.get("asset") new_task_name = instance.get("task") + if variant_value is not None: + new_variant_value = variant_value + + if asset_name is not None: + new_asset_name = asset_name + + if task_name is not None: + new_task_name = task_name asset_doc = asset_docs_by_name[new_asset_name] @@ -1026,7 +1058,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): new_variant_value, new_task_name, asset_doc, project_name ) except TaskNotSetError: + invalid_tasks = True instance.set_task_invalid(True) + subset_names.add(instance["subset"]) continue subset_names.add(new_subset_name) @@ -1043,10 +1077,13 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance["subset"] = new_subset_name + if invalid_tasks: + self.task_value_widget.set_invalid_empty_task() + self.subset_value_widget.set_value(subset_names) self._set_btns_enabled(False) - self._set_btns_visible(False) + self._set_btns_visible(invalid_tasks) self.instance_context_changed.emit() From 41bee1d2fac264cac48d3d5f6d2edd5139c5e2b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 18:49:57 +0100 Subject: [PATCH 24/29] change checkstate of goups on key press events --- .../tools/publisher/widgets/list_view_widgets.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 23a86cd070..6bddaf66c8 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -467,12 +467,22 @@ class InstanceListView(AbstractInstanceView): else: active = False + group_names = set() for instance_id in selected_instance_ids: widget = self._widgets_by_id.get(instance_id) - if widget is not None: - widget.set_active(active) + if widget is None: + continue + + widget.set_active(active) + group_name = self._group_by_instance_id.get(instance_id) + if group_name is not None: + group_names.add(group_name) + + for group_name in group_names: + self._update_group_checkstate(group_name) def _update_group_checkstate(self, group_name): + """Update checkstate of one group.""" widget = self._group_widgets.get(group_name) if widget is None: return From 5a2f917c2aec35286efe030f5b4f0c28c3fb293d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 18:54:54 +0100 Subject: [PATCH 25/29] empty task is stored as None --- openpype/tools/publisher/widgets/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index ace7b4e02d..200e85ba5c 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1072,7 +1072,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance.set_asset_invalid(False) if task_name is not None: - instance["task"] = task_name + instance["task"] = task_name or None instance.set_task_invalid(False) instance["subset"] = new_subset_name From 86ddbf8c5cf5455249cfe8d11d58593333190a5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 18:56:35 +0100 Subject: [PATCH 26/29] task as none will use empty string --- openpype/tools/publisher/widgets/widgets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index 200e85ba5c..8a950feb8b 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -1156,9 +1156,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): variants.add(instance.get("variant") or self.unknown_value) families.add(instance.get("family") or self.unknown_value) asset_name = instance.get("asset") or self.unknown_value - task_name = instance.get("task") - if task_name is None: - task_name = self.unknown_value + task_name = instance.get("task") or "" asset_names.add(asset_name) asset_task_combinations.append((asset_name, task_name)) subset_names.add(instance.get("subset") or self.unknown_value) From a7b8cf26260f60c69d212dae7fb611c46503da72 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 19:20:04 +0100 Subject: [PATCH 27/29] add plugin to report before it's processing --- openpype/tools/publisher/control.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 5a84b1d8ca..6707feac9c 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -873,8 +873,6 @@ class PublisherController: """ for idx, plugin in enumerate(self.publish_plugins): self._publish_progress = idx - # Add plugin to publish report - self._publish_report.add_plugin_iter(plugin, self._publish_context) # Reset current plugin validations error self._publish_current_plugin_validation_errors = None @@ -902,6 +900,9 @@ class PublisherController: ): yield MainThreadItem(self.stop_publish) + # Add plugin to publish report + self._publish_report.add_plugin_iter(plugin, self._publish_context) + # Trigger callback that new plugin is going to be processed self._trigger_callbacks( self._publish_plugin_changed_callback_refs, plugin From 23b5f3828a4d248590f1035875eb05d0f674fea4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 9 Mar 2022 19:20:23 +0100 Subject: [PATCH 28/29] don't use task if is not available in intergrate new --- openpype/plugins/publish/integrate_new.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6e0940d459..33d365fe42 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -192,11 +192,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "short": task_code } - else: + elif "task" in anatomy_data: # Just set 'task_name' variable to context task task_name = anatomy_data["task"]["name"] task_type = anatomy_data["task"]["type"] + else: + task_name = None + task_type = None + # Fill family in anatomy data anatomy_data["family"] = instance.data.get("family") @@ -816,8 +820,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # - is there a chance that task name is not filled in anatomy # data? # - should we use context task in that case? - task_name = instance.data["anatomyData"]["task"]["name"] - task_type = instance.data["anatomyData"]["task"]["type"] + anatomy_data = instance.data["anatomyData"] + task_name = None + task_type = None + if "task" in anatomy_data: + task_name = anatomy_data["task"]["name"] + task_type = anatomy_data["task"]["type"] filtering_criteria = { "families": instance.data["family"], "hosts": instance.context.data["hostName"], From ca94cf247aceac8a3c61d38465625c9c82c85aed Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 10 Mar 2022 09:37:42 +0100 Subject: [PATCH 29/29] add refactor sections to CI --- .github/workflows/prerelease.yml | 2 +- .github/workflows/release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 258458e2d4..d9b4d8089c 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,7 +43,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f85525c26..917e6c884c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]},"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" @@ -81,7 +81,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: ${{ steps.version.outputs.last_release }}