From e629e40b4f95718e94de5c63180c38251c3c8e54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Mar 2022 18:58:33 +0100 Subject: [PATCH 01/27] created base of event system --- openpype/pipeline/__init__.py | 8 ++ openpype/pipeline/events.py | 221 ++++++++++++++++++++++++++++++ openpype/pipeline/lib/__init__.py | 8 -- openpype/pipeline/lib/events.py | 51 ------- 4 files changed, 229 insertions(+), 59 deletions(-) create mode 100644 openpype/pipeline/events.py delete mode 100644 openpype/pipeline/lib/events.py diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index e968df4011..673608bded 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -1,5 +1,10 @@ from .lib import attribute_definitions +from .events import ( + emit_event, + register_event_callback +) + from .create import ( BaseCreator, Creator, @@ -17,6 +22,9 @@ from .publish import ( __all__ = ( "attribute_definitions", + "emit_event", + "register_event_callback", + "BaseCreator", "Creator", "AutoCreator", diff --git a/openpype/pipeline/events.py b/openpype/pipeline/events.py new file mode 100644 index 0000000000..cae8b250f7 --- /dev/null +++ b/openpype/pipeline/events.py @@ -0,0 +1,221 @@ +"""Events holding data about specific event.""" +import os +import re +import inspect +import logging +import weakref +from uuid import uuid4 +try: + from weakref import WeakMethod +except Exception: + from .python_2_comp import WeakMethod + + +class EventCallback(object): + def __init__(self, topic, func_ref, func_name, func_path): + self._topic = topic + # Replace '*' with any character regex and escape rest of text + # - when callback is registered for '*' topic it will receive all + # events + # - it is possible to register to a partial topis 'my.event.*' + # - it will receive all matching event topics + # e.g. 'my.event.start' and 'my.event.end' + topic_regex_str = "^{}$".format( + ".+".join( + re.escape(part) + for part in topic.split("*") + ) + ) + topic_regex = re.compile(topic_regex_str) + self._topic_regex = topic_regex + self._func_ref = func_ref + self._func_name = func_name + self._func_path = func_path + self._ref_valid = True + self._enabled = True + + self._log = None + + def __repr__(self): + return "< {} - {} > {}".format( + self.__class__.__name__, self._func_name, self._func_path + ) + + @property + def log(self): + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + + @property + def is_ref_valid(self): + return self._ref_valid + + def validate_ref(self): + if not self._ref_valid: + return + + callback = self._func_ref() + if not callback: + self._ref_valid = False + + @property + def enabled(self): + """Is callback enabled.""" + return self._enabled + + def set_enabled(self, enabled): + """Change if callback is enabled.""" + self._enabled = enabled + + def deregister(self): + """Calling this funcion will cause that callback will be removed.""" + # Fake reference + self._ref_valid = False + + def topic_matches(self, topic): + """Check if event topic matches callback's topic.""" + return self._topic_regex.match(topic) + + def process_event(self, event): + """Process event. + + Args: + event(Event): Event that was triggered. + """ + # Skip if callback is not enabled or has invalid reference + if not self._ref_valid or not self._enabled: + return + + # Get reference + callback = self._func_ref() + # Check if reference is valid or callback's topic matches the event + if not callback: + # Change state if is invalid so the callback is removed + self._ref_valid = False + + elif self.topic_matches(event.topic): + # Try execute callback + sig = inspect.signature(callback) + try: + if len(sig.parameters) == 0: + callback() + else: + callback(event) + except Exception: + self.log.warning( + "Failed to execute event callback {}".format( + str(repr(self)) + ), + exc_info=True + ) + + +# Inherit from 'object' for Python 2 hosts +class Event(object): + """Base event object. + + Can be used to anything because data are not much specific. Only required + argument is topic which defines why event is happening and may be used for + filtering. + + Arg: + topic (str): Identifier of event. + data (Any): Data specific for event. Dictionary is recommended. + """ + _data = {} + + def __init__(self, topic, data=None, source=None): + self._id = str(uuid4()) + self._topic = topic + if data is None: + data = {} + self._data = data + self._source = source + + def __getitem__(self, key): + return self._data[key] + + def get(self, key, *args, **kwargs): + return self._data.get(key, *args, **kwargs) + + @property + def id(self): + return self._id + + @property + def source(self): + return self._source + + @property + def data(self): + return self._data + + @property + def topic(self): + return self._topic + + def emit(self): + """Emit event and trigger callbacks.""" + StoredCallbacks.emit_event(self) + + +class StoredCallbacks: + _registered_callbacks = [] + + @classmethod + def add_callback(cls, topic, callback): + # Convert callback into references + # - deleted functions won't cause crashes + if inspect.ismethod(callback): + ref = WeakMethod(callback) + elif callable(callback): + ref = weakref.ref(callback) + else: + # TODO add logs + return + + function_name = callback.__name__ + function_path = os.path.abspath(inspect.getfile(callback)) + callback = EventCallback(topic, ref, function_name, function_path) + cls._registered_callbacks.append(callback) + return callback + + @classmethod + def validate(cls): + invalid_callbacks = [] + for callbacks in cls._registered_callbacks: + for callback in tuple(callbacks): + callback.validate_ref() + if not callback.is_ref_valid: + invalid_callbacks.append(callback) + + for callback in invalid_callbacks: + cls._registered_callbacks.remove(callback) + + @classmethod + def emit_event(cls, event): + invalid_callbacks = [] + for callback in cls._registered_callbacks: + callback.process_event() + if not callback.is_ref_valid: + invalid_callbacks.append(callback) + + for callback in invalid_callbacks: + cls._registered_callbacks.remove(callback) + + +def register_event_callback(topic, callback): + """Add callback that will be executed on specific topic.""" + return StoredCallbacks.add_callback(topic, callback) + + +def emit_event(topic, data=None, source=None): + """Emit event with topic and data. + + Returns: + Event: Object of event that was emitted. + """ + event = Event(topic, data, source) + event.emit() + return event diff --git a/openpype/pipeline/lib/__init__.py b/openpype/pipeline/lib/__init__.py index ed38889c66..f762c4205d 100644 --- a/openpype/pipeline/lib/__init__.py +++ b/openpype/pipeline/lib/__init__.py @@ -1,8 +1,3 @@ -from .events import ( - BaseEvent, - BeforeWorkfileSave -) - from .attribute_definitions import ( AbtractAttrDef, @@ -20,9 +15,6 @@ from .attribute_definitions import ( __all__ = ( - "BaseEvent", - "BeforeWorkfileSave", - "AbtractAttrDef", "UIDef", diff --git a/openpype/pipeline/lib/events.py b/openpype/pipeline/lib/events.py deleted file mode 100644 index 05dea20e8c..0000000000 --- a/openpype/pipeline/lib/events.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Events holding data about specific event.""" - - -# Inherit from 'object' for Python 2 hosts -class BaseEvent(object): - """Base event object. - - Can be used to anything because data are not much specific. Only required - argument is topic which defines why event is happening and may be used for - filtering. - - Arg: - topic (str): Identifier of event. - data (Any): Data specific for event. Dictionary is recommended. - """ - _data = {} - - def __init__(self, topic, data=None): - self._topic = topic - if data is None: - data = {} - self._data = data - - @property - def data(self): - return self._data - - @property - def topic(self): - return self._topic - - @classmethod - def emit(cls, *args, **kwargs): - """Create object of event and emit. - - Args: - Same args as '__init__' expects which may be class specific. - """ - from avalon import pipeline - - obj = cls(*args, **kwargs) - pipeline.emit(obj.topic, [obj]) - return obj - - -class BeforeWorkfileSave(BaseEvent): - """Before workfile changes event data.""" - def __init__(self, filename, workdir): - super(BeforeWorkfileSave, self).__init__("before.workfile.save") - self.filename = filename - self.workdir_path = workdir From 9c111fa9d448b9371807f7812c03d21c4aa5b6f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Mar 2022 19:01:31 +0100 Subject: [PATCH 02/27] use new event system in openpype --- openpype/__init__.py | 7 +-- openpype/hosts/aftereffects/api/pipeline.py | 3 +- openpype/hosts/blender/api/pipeline.py | 25 ++++++---- openpype/hosts/harmony/api/pipeline.py | 5 +- openpype/hosts/hiero/api/events.py | 4 +- openpype/hosts/hiero/api/menu.py | 2 +- openpype/hosts/houdini/api/pipeline.py | 30 +++++++----- openpype/hosts/maya/api/pipeline.py | 49 ++++++++++--------- openpype/hosts/nuke/api/pipeline.py | 7 +-- openpype/hosts/photoshop/api/pipeline.py | 4 +- .../hosts/tvpaint/api/communication_server.py | 6 +-- openpype/hosts/tvpaint/api/pipeline.py | 5 +- openpype/hosts/webpublisher/api/__init__.py | 5 -- openpype/tools/loader/app.py | 5 +- openpype/tools/workfiles/app.py | 16 ++++-- 15 files changed, 97 insertions(+), 76 deletions(-) diff --git a/openpype/__init__.py b/openpype/__init__.py index 11b563ebfe..9175727a54 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -10,8 +10,9 @@ from .lib import ( Anatomy, filter_pyblish_plugins, set_plugin_attributes_from_settings, - change_timer_to_current_context + change_timer_to_current_context, ) +from .pipeline import register_event_callback pyblish = avalon = _original_discover = None @@ -122,10 +123,10 @@ def install(): avalon.discover = patched_discover pipeline.discover = patched_discover - avalon.on("taskChanged", _on_task_change) + register_event_callback("taskChanged", _on_task_change) -def _on_task_change(*args): +def _on_task_change(): change_timer_to_current_context() diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 94f1e3d105..6f7cd8c46d 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -10,6 +10,7 @@ from avalon import io, pipeline from openpype import lib from openpype.api import Logger import openpype.hosts.aftereffects +from openpype.pipeline import register_event_callback from .launch_logic import get_stub @@ -73,7 +74,7 @@ def install(): "instanceToggled", on_pyblish_instance_toggled ) - avalon.api.on("application.launched", application_launch) + register_event_callback("application.launched", application_launch) def uninstall(): diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 6da0ba3dcb..38312316cc 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -15,6 +15,10 @@ from avalon import io, schema from avalon.pipeline import AVALON_CONTAINER_ID from openpype.api import Logger +from openpype.pipeline import ( + register_event_callback, + emit_event +) import openpype.hosts.blender HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__)) @@ -50,8 +54,9 @@ def install(): lib.append_user_scripts() - avalon.api.on("new", on_new) - avalon.api.on("open", on_open) + register_event_callback("new", on_new) + register_event_callback("open", on_open) + _register_callbacks() _register_events() @@ -113,22 +118,22 @@ def set_start_end_frames(): scene.render.resolution_y = resolution_y -def on_new(arg1, arg2): +def on_new(): set_start_end_frames() -def on_open(arg1, arg2): +def on_open(): set_start_end_frames() @bpy.app.handlers.persistent def _on_save_pre(*args): - avalon.api.emit("before_save", args) + emit_event("before.save") @bpy.app.handlers.persistent def _on_save_post(*args): - avalon.api.emit("save", args) + emit_event("save") @bpy.app.handlers.persistent @@ -136,9 +141,9 @@ def _on_load_post(*args): # Detect new file or opening an existing file if bpy.data.filepath: # Likely this was an open operation since it has a filepath - avalon.api.emit("open", args) + emit_event("open") else: - avalon.api.emit("new", args) + emit_event("new") ops.OpenFileCacher.post_load() @@ -169,7 +174,7 @@ def _register_callbacks(): log.info("Installed event handler _on_load_post...") -def _on_task_changed(*args): +def _on_task_changed(): """Callback for when the task in the context is changed.""" # TODO (jasper): Blender has no concept of projects or workspace. @@ -186,7 +191,7 @@ def _on_task_changed(*args): def _register_events(): """Install callbacks for specific events.""" - avalon.api.on("taskChanged", _on_task_changed) + register_event_callback("taskChanged", _on_task_changed) log.info("Installed event callback for 'taskChanged'...") diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 17d2870876..a94d30210e 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 register_event_callback import openpype.hosts.harmony import openpype.hosts.harmony.api as harmony @@ -129,7 +130,7 @@ def check_inventory(): harmony.send({"function": "PypeHarmony.message", "args": msg}) -def application_launch(): +def application_launch(event): """Event that is executed after Harmony is launched.""" # FIXME: This is breaking server <-> client communication. # It is now moved so it it manually called. @@ -187,7 +188,7 @@ def install(): "instanceToggled", on_pyblish_instance_toggled ) - avalon.api.on("application.launched", application_launch) + register_event_callback("application.launched", application_launch) def uninstall(): diff --git a/openpype/hosts/hiero/api/events.py b/openpype/hosts/hiero/api/events.py index 7563503593..6e2580ed8c 100644 --- a/openpype/hosts/hiero/api/events.py +++ b/openpype/hosts/hiero/api/events.py @@ -1,7 +1,7 @@ import os import hiero.core.events -import avalon.api as avalon from openpype.api import Logger +from openpype.pipeline import register_event_callback from .lib import ( sync_avalon_data_to_workfile, launch_workfiles_app, @@ -126,5 +126,5 @@ def register_events(): """ # if task changed then change notext of hiero - avalon.on("taskChanged", update_menu_task_label) + register_event_callback("taskChanged", update_menu_task_label) log.info("Installed event callback for 'taskChanged'..") diff --git a/openpype/hosts/hiero/api/menu.py b/openpype/hosts/hiero/api/menu.py index 306bef87ca..de20b86f30 100644 --- a/openpype/hosts/hiero/api/menu.py +++ b/openpype/hosts/hiero/api/menu.py @@ -14,7 +14,7 @@ self = sys.modules[__name__] self._change_context_menu = None -def update_menu_task_label(*args): +def update_menu_task_label(): """Update the task label in Avalon menu to current session""" object_name = self._change_context_menu diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 1c08e72d65..86c85ad3a1 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -11,6 +11,10 @@ import avalon.api from avalon.pipeline import AVALON_CONTAINER_ID from avalon.lib import find_submodule +from openpype.pipeline import ( + register_event_callback, + emit_event +) import openpype.hosts.houdini from openpype.hosts.houdini.api import lib @@ -51,11 +55,11 @@ def install(): avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) log.info("Installing callbacks ... ") - # avalon.on("init", on_init) - avalon.api.before("save", before_save) - avalon.api.on("save", on_save) - avalon.api.on("open", on_open) - avalon.api.on("new", on_new) + # register_event_callback("init", on_init) + register_event_callback("before.save", before_save) + register_event_callback("save", on_save) + register_event_callback("open", on_open) + register_event_callback("new", on_new) pyblish.api.register_callback( "instanceToggled", on_pyblish_instance_toggled @@ -101,13 +105,13 @@ def _register_callbacks(): def on_file_event_callback(event): if event == hou.hipFileEventType.AfterLoad: - avalon.api.emit("open", [event]) + emit_event("open") elif event == hou.hipFileEventType.AfterSave: - avalon.api.emit("save", [event]) + emit_event("save") elif event == hou.hipFileEventType.BeforeSave: - avalon.api.emit("before_save", [event]) + emit_event("before_save") elif event == hou.hipFileEventType.AfterClear: - avalon.api.emit("new", [event]) + emit_event("new") def get_main_window(): @@ -229,11 +233,11 @@ def ls(): yield data -def before_save(*args): +def before_save(): return lib.validate_fps() -def on_save(*args): +def on_save(): log.info("Running callback on save..") @@ -242,7 +246,7 @@ def on_save(*args): lib.set_id(node, new_id, overwrite=False) -def on_open(*args): +def on_open(): if not hou.isUIAvailable(): log.debug("Batch mode detected, ignoring `on_open` callbacks..") @@ -279,7 +283,7 @@ def on_open(*args): dialog.show() -def on_new(_): +def on_new(): """Set project resolution and fps when create a new file""" if hou.hipFile.isLoadingHipFile(): diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 1b3bb9feb3..05db1b7b26 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 @@ -16,6 +15,10 @@ from avalon.pipeline import AVALON_CONTAINER_ID import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import any_outdated +from openpype.pipeline import ( + register_event_callback, + emit_event +) from openpype.lib.path_tools import HostDirmap from openpype.hosts.maya.lib import copy_workspace_mel from . import menu, lib @@ -55,7 +58,7 @@ def install(): log.info(PUBLISH_PATH) log.info("Installing callbacks ... ") - avalon.api.on("init", on_init) + register_event_callback("init", on_init) # Callbacks below are not required for headless mode, the `init` however # is important to load referenced Alembics correctly at rendertime. @@ -69,12 +72,12 @@ def install(): menu.install() - avalon.api.on("save", on_save) - avalon.api.on("open", on_open) - avalon.api.on("new", on_new) - avalon.api.before("save", on_before_save) - avalon.api.on("taskChanged", on_task_changed) - avalon.api.on("before.workfile.save", before_workfile_save) + register_event_callback("save", on_save) + register_event_callback("open", on_open) + register_event_callback("new", on_new) + register_event_callback("before.save", on_before_save) + register_event_callback("taskChanged", on_task_changed) + register_event_callback("before.workfile.save", before_workfile_save) def _set_project(): @@ -137,7 +140,7 @@ def _register_callbacks(): def _on_maya_initialized(*args): - avalon.api.emit("init", args) + emit_event("init") if cmds.about(batch=True): log.warning("Running batch mode ...") @@ -147,16 +150,16 @@ def _on_maya_initialized(*args): lib.get_main_window() -def _on_scene_new(*args): - avalon.api.emit("new", args) +def _on_scene_new(): + emit_event("new") -def _on_scene_save(*args): - avalon.api.emit("save", args) +def _on_scene_save(): + emit_event("save") -def _on_scene_open(*args): - avalon.api.emit("open", args) +def _on_scene_open(): + emit_event("open") def _before_scene_save(return_code, client_data): @@ -166,7 +169,7 @@ def _before_scene_save(return_code, client_data): # in order to block the operation. OpenMaya.MScriptUtil.setBool(return_code, True) - avalon.api.emit("before_save", [return_code, client_data]) + emit_event("before.save") def uninstall(): @@ -343,7 +346,7 @@ def containerise(name, return container -def on_init(_): +def on_init(): log.info("Running callback on init..") def safe_deferred(fn): @@ -384,12 +387,12 @@ def on_init(_): safe_deferred(override_toolbox_ui) -def on_before_save(return_code, _): +def on_before_save(): """Run validation for scene's FPS prior to saving""" return lib.validate_fps() -def on_save(_): +def on_save(): """Automatically add IDs to new nodes Any transform of a mesh, without an existing ID, is given one @@ -407,7 +410,7 @@ def on_save(_): lib.set_id(node, new_id, overwrite=False) -def on_open(_): +def on_open(): """On scene open let's assume the containers have changed.""" from Qt import QtWidgets @@ -455,7 +458,7 @@ def on_open(_): dialog.show() -def on_new(_): +def on_new(): """Set project resolution and fps when create a new file""" log.info("Running callback on new..") with lib.suspended_refresh(): @@ -471,7 +474,7 @@ def on_new(_): lib.set_context_settings() -def on_task_changed(*args): +def on_task_changed(): """Wrapped function of app initialize and maya's on task changed""" # Run menu.update_menu_task_label() @@ -509,7 +512,7 @@ def on_task_changed(*args): def before_workfile_save(event): - workdir_path = event.workdir_path + workdir_path = event["workdir_path"] if workdir_path: copy_workspace_mel(workdir_path) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 8c6c9ca55b..1a5116c9ea 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 register_event_callback from openpype.tools.utils import host_tools from .command import viewer_update_and_undo_stop @@ -102,8 +103,8 @@ def install(): avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) # Register Avalon event for workfiles loading. - avalon.api.on("workio.open_file", check_inventory_versions) - avalon.api.on("taskChanged", change_context_label) + register_event_callback("workio.open_file", check_inventory_versions) + register_event_callback("taskChanged", change_context_label) pyblish.api.register_callback( "instanceToggled", on_pyblish_instance_toggled) @@ -226,7 +227,7 @@ def _uninstall_menu(): menu.removeItem(item.name()) -def change_context_label(*args): +def change_context_label(): menubar = nuke.menu("Nuke") menu = menubar.findItem(MENU_LABEL) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 25983f2471..2aade59812 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 register_event_callback import openpype.hosts.photoshop from . import lib @@ -75,7 +75,7 @@ def install(): "instanceToggled", on_pyblish_instance_toggled ) - avalon.api.on("application.launched", on_application_launch) + register_event_callback("application.launched", on_application_launch) def uninstall(): diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index e9c5f4c73e..b001b84203 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -21,7 +21,7 @@ from aiohttp_json_rpc.protocol import ( ) from aiohttp_json_rpc.exceptions import RpcError -from avalon import api +from openpype.pipeline import emit_event from openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path log = logging.getLogger(__name__) @@ -754,7 +754,7 @@ class BaseCommunicator: self._on_client_connect() - api.emit("application.launched") + emit_event("application.launched") def _on_client_connect(self): self._initial_textfile_write() @@ -938,5 +938,5 @@ class QtCommunicator(BaseCommunicator): def _exit(self, *args, **kwargs): super()._exit(*args, **kwargs) - api.emit("application.exit") + emit_event("application.exit") self.qt_app.exit(self.exit_code) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 74eb41892c..b999478fb1 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 register_event_callback from .lib import ( execute_george, @@ -84,8 +85,8 @@ def install(): if on_instance_toggle not in registered_callbacks: pyblish.api.register_callback("instanceToggled", on_instance_toggle) - avalon.api.on("application.launched", initial_launch) - avalon.api.on("application.exit", application_exit) + register_event_callback("application.launched", initial_launch) + register_event_callback("application.exit", application_exit) def uninstall(): diff --git a/openpype/hosts/webpublisher/api/__init__.py b/openpype/hosts/webpublisher/api/__init__.py index e40d46d662..f338c92a5e 100644 --- a/openpype/hosts/webpublisher/api/__init__.py +++ b/openpype/hosts/webpublisher/api/__init__.py @@ -16,10 +16,6 @@ LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") -def application_launch(): - pass - - def install(): print("Installing Pype config...") @@ -29,7 +25,6 @@ def install(): log.info(PUBLISH_PATH) io.install() - avalon.on("application.launched", application_launch) def uninstall(): diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index aa743b05fe..afb94bf8fc 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -1,9 +1,10 @@ import sys from Qt import QtWidgets, QtCore -from avalon import api, io, pipeline +from avalon import api, io from openpype import style +from openpype.pipeline import register_event_callback from openpype.tools.utils import ( lib, PlaceholderLineEdit @@ -33,7 +34,7 @@ def on_context_task_change(*args, **kwargs): module.window.on_context_task_change(*args, **kwargs) -pipeline.on("taskChanged", on_context_task_change) +register_event_callback("taskChanged", on_context_task_change) class LoaderWindow(QtWidgets.QDialog): diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index aece7bfb4f..280fe2d8a2 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -9,10 +9,10 @@ import datetime import Qt from Qt import QtWidgets, QtCore -from avalon import io, api, pipeline +from avalon import io, api from openpype import style -from openpype.pipeline.lib import BeforeWorkfileSave +from openpype.pipeline import emit_event from openpype.tools.utils.lib import ( qt_app_context ) @@ -823,7 +823,11 @@ class FilesWidget(QtWidgets.QWidget): return # Trigger before save event - BeforeWorkfileSave.emit(work_filename, self._workdir_path) + emit_event( + "before.workfile.save", + {"filename": work_filename, "workdir_path": self._workdir_path}, + source="workfiles.tool" + ) # Make sure workfiles root is updated # - this triggers 'workio.work_root(...)' which may change value of @@ -853,7 +857,11 @@ class FilesWidget(QtWidgets.QWidget): api.Session["AVALON_PROJECT"] ) # Trigger after save events - pipeline.emit("after.workfile.save", [filepath]) + emit_event( + "after.workfile.save", + {"filepath": filepath}, + source="workfiles.tool" + ) self.workfile_created.emit(filepath) # Refresh files model From 502785021e5fcf364bb1b01217c1dc522114d17e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 5 Mar 2022 08:30:29 +0100 Subject: [PATCH 03/27] moved events to openpype.lib --- openpype/__init__.py | 2 +- openpype/hosts/aftereffects/api/pipeline.py | 2 +- openpype/hosts/blender/api/pipeline.py | 2 +- openpype/hosts/harmony/api/pipeline.py | 2 +- openpype/hosts/hiero/api/events.py | 4 ++-- openpype/hosts/houdini/api/pipeline.py | 8 +++----- openpype/hosts/maya/api/pipeline.py | 10 +++++----- openpype/hosts/nuke/api/pipeline.py | 2 +- openpype/hosts/photoshop/api/pipeline.py | 2 +- openpype/hosts/tvpaint/api/communication_server.py | 2 +- openpype/hosts/tvpaint/api/pipeline.py | 2 +- openpype/lib/__init__.py | 7 +++++++ openpype/{pipeline => lib}/events.py | 11 ++++++++--- openpype/pipeline/__init__.py | 8 -------- openpype/tools/loader/app.py | 2 +- openpype/tools/workfiles/app.py | 2 +- 16 files changed, 35 insertions(+), 33 deletions(-) rename openpype/{pipeline => lib}/events.py (94%) diff --git a/openpype/__init__.py b/openpype/__init__.py index 9175727a54..63ad81d3ca 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -11,8 +11,8 @@ from .lib import ( filter_pyblish_plugins, set_plugin_attributes_from_settings, change_timer_to_current_context, + register_event_callback, ) -from .pipeline import register_event_callback pyblish = avalon = _original_discover = None diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 6f7cd8c46d..96d04aad14 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -10,7 +10,7 @@ from avalon import io, pipeline from openpype import lib from openpype.api import Logger import openpype.hosts.aftereffects -from openpype.pipeline import register_event_callback +from openpype.lib import register_event_callback from .launch_logic import get_stub diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 38312316cc..d1e7df3a93 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -15,7 +15,7 @@ from avalon import io, schema from avalon.pipeline import AVALON_CONTAINER_ID from openpype.api import Logger -from openpype.pipeline import ( +from openpype.lib import ( register_event_callback, emit_event ) diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index a94d30210e..8d7ac19eb9 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -9,7 +9,7 @@ import avalon.api from avalon.pipeline import AVALON_CONTAINER_ID from openpype import lib -from openpype.pipeline import register_event_callback +from openpype.lib import register_event_callback import openpype.hosts.harmony import openpype.hosts.harmony.api as harmony diff --git a/openpype/hosts/hiero/api/events.py b/openpype/hosts/hiero/api/events.py index 6e2580ed8c..9439199933 100644 --- a/openpype/hosts/hiero/api/events.py +++ b/openpype/hosts/hiero/api/events.py @@ -1,12 +1,12 @@ import os import hiero.core.events from openpype.api import Logger -from openpype.pipeline import register_event_callback from .lib import ( sync_avalon_data_to_workfile, launch_workfiles_app, selection_changed_timeline, - before_project_save + before_project_save, + register_event_callback ) from .tags import add_tags_to_workfile from .menu import update_menu_task_label diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 86c85ad3a1..bbb7a7c512 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -11,15 +11,13 @@ import avalon.api from avalon.pipeline import AVALON_CONTAINER_ID from avalon.lib import find_submodule -from openpype.pipeline import ( - register_event_callback, - emit_event -) import openpype.hosts.houdini from openpype.hosts.houdini.api import lib from openpype.lib import ( - any_outdated + register_event_callback, + emit_event, + any_outdated, ) from .lib import get_asset_fps diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 05db1b7b26..4945a9ba56 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -14,8 +14,8 @@ from avalon.pipeline import AVALON_CONTAINER_ID import openpype.hosts.maya from openpype.tools.utils import host_tools -from openpype.lib import any_outdated -from openpype.pipeline import ( +from openpype.lib import ( + any_outdated, register_event_callback, emit_event ) @@ -150,15 +150,15 @@ def _on_maya_initialized(*args): lib.get_main_window() -def _on_scene_new(): +def _on_scene_new(*args): emit_event("new") -def _on_scene_save(): +def _on_scene_save(*args): emit_event("save") -def _on_scene_open(): +def _on_scene_open(*args): emit_event("open") diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 1a5116c9ea..419cfb1ac2 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -14,7 +14,7 @@ from openpype.api import ( BuildWorkfile, get_current_project_settings ) -from openpype.pipeline import register_event_callback +from openpype.lib import register_event_callback from openpype.tools.utils import host_tools from .command import viewer_update_and_undo_stop diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 2aade59812..e424400465 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -6,7 +6,7 @@ import avalon.api from avalon import pipeline, io from openpype.api import Logger -from openpype.pipeline import register_event_callback +from openpype.lib import register_event_callback import openpype.hosts.photoshop from . import lib diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index b001b84203..65cb9aa2f3 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -21,7 +21,7 @@ from aiohttp_json_rpc.protocol import ( ) from aiohttp_json_rpc.exceptions import RpcError -from openpype.pipeline import emit_event +from openpype.lib import emit_event from openpype.hosts.tvpaint.tvpaint_plugin import get_plugin_files_path log = logging.getLogger(__name__) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index b999478fb1..381c2c62f0 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -14,7 +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 register_event_callback +from openpype.lib import register_event_callback from .lib import ( execute_george, diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 6a24f30455..1ee9129fa7 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -16,6 +16,10 @@ sys.path.insert(0, python_version_dir) site.addsitedir(python_version_dir) +from .events import ( + emit_event, + register_event_callback +) from .env_tools import ( env_value_to_bool, get_paths_from_environ, @@ -193,6 +197,9 @@ from .openpype_version import ( terminal = Terminal __all__ = [ + "emit_event", + "register_event_callback", + "get_openpype_execute_args", "get_pype_execute_args", "get_linux_launcher_args", diff --git a/openpype/pipeline/events.py b/openpype/lib/events.py similarity index 94% rename from openpype/pipeline/events.py rename to openpype/lib/events.py index cae8b250f7..62c480d8e0 100644 --- a/openpype/pipeline/events.py +++ b/openpype/lib/events.py @@ -8,7 +8,7 @@ from uuid import uuid4 try: from weakref import WeakMethod except Exception: - from .python_2_comp import WeakMethod + from openpype.lib.python_2_comp import WeakMethod class EventCallback(object): @@ -83,6 +83,7 @@ class EventCallback(object): Args: event(Event): Event that was triggered. """ + self.log.info("Processing event {}".format(event.topic)) # Skip if callback is not enabled or has invalid reference if not self._ref_valid or not self._enabled: return @@ -93,9 +94,11 @@ class EventCallback(object): if not callback: # Change state if is invalid so the callback is removed self._ref_valid = False + self.log.info("Invalid reference") elif self.topic_matches(event.topic): # Try execute callback + self.log.info("Triggering callback") sig = inspect.signature(callback) try: if len(sig.parameters) == 0: @@ -109,6 +112,8 @@ class EventCallback(object): ), exc_info=True ) + else: + self.log.info("Not matchin callback") # Inherit from 'object' for Python 2 hosts @@ -172,7 +177,7 @@ class StoredCallbacks: elif callable(callback): ref = weakref.ref(callback) else: - # TODO add logs + print("Invalid callback") return function_name = callback.__name__ @@ -197,7 +202,7 @@ class StoredCallbacks: def emit_event(cls, event): invalid_callbacks = [] for callback in cls._registered_callbacks: - callback.process_event() + callback.process_event(event) if not callback.is_ref_valid: invalid_callbacks.append(callback) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 673608bded..e968df4011 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -1,10 +1,5 @@ from .lib import attribute_definitions -from .events import ( - emit_event, - register_event_callback -) - from .create import ( BaseCreator, Creator, @@ -22,9 +17,6 @@ from .publish import ( __all__ = ( "attribute_definitions", - "emit_event", - "register_event_callback", - "BaseCreator", "Creator", "AutoCreator", diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index afb94bf8fc..ec8f56e74b 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -4,7 +4,7 @@ from Qt import QtWidgets, QtCore from avalon import api, io from openpype import style -from openpype.pipeline import register_event_callback +from openpype.lib import register_event_callback from openpype.tools.utils import ( lib, PlaceholderLineEdit diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 280fe2d8a2..87e1492a20 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -12,7 +12,6 @@ from Qt import QtWidgets, QtCore from avalon import io, api from openpype import style -from openpype.pipeline import emit_event from openpype.tools.utils.lib import ( qt_app_context ) @@ -21,6 +20,7 @@ from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget from openpype.tools.utils.tasks_widget import TasksWidget from openpype.tools.utils.delegates import PrettyTimeDelegate from openpype.lib import ( + emit_event, Anatomy, get_workfile_doc, create_workfile_doc, From 70cf30041aa581f65fd5227cae508e42b7aeac88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Sat, 5 Mar 2022 09:45:36 +0100 Subject: [PATCH 04/27] fix python 2 compatibility --- openpype/lib/events.py | 89 ++++++++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/openpype/lib/events.py b/openpype/lib/events.py index 62c480d8e0..f71cb74c10 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -12,7 +12,32 @@ except Exception: class EventCallback(object): - def __init__(self, topic, func_ref, func_name, func_path): + """Callback registered to a topic. + + The callback function is registered to a topic. Topic is a string which + may contain '*' that will be handled as "any characters". + + # Examples: + - "workfile.save" Callback will be triggered if the event topic is exactly + "workfile.save" . + - "workfile.*" Callback will be triggered an event topic starts with + "workfile." so "workfile.save" and "workfile.open" + will trigger the callback. + - "*" Callback will listen to all events. + + Callback can be function or method. In both cases it should expect one + or none arguments. When 1 argument is expected then the processed 'Event' + object is passed in. + + The registered callbacks don't keep function in memory so it is not + possible to store lambda function as callback. + + Args: + topic(str): Topic which will be listened. + func(func): Callback to a topic. + """ + def __init__(self, topic, func): + self._log = None self._topic = topic # Replace '*' with any character regex and escape rest of text # - when callback is registered for '*' topic it will receive all @@ -28,14 +53,42 @@ class EventCallback(object): ) topic_regex = re.compile(topic_regex_str) self._topic_regex = topic_regex + + # Convert callback into references + # - deleted functions won't cause crashes + if inspect.ismethod(func): + func_ref = WeakMethod(func) + elif callable(func): + func_ref = weakref.ref(func) + else: + func_ref = None + self.log.warning(( + "Registered callback is not callable. \"{}\"" + ).format(str(func))) + + func_name = None + func_path = None + expect_args = False + # Collect additional data about function + # - name + # - path + # - if expect argument or not + if func_ref is not None: + func_name = func.__name__ + func_path = os.path.abspath(inspect.getfile(func)) + if hasattr(inspect, "signature"): + sig = inspect.signature(func) + expect_args = len(sig.parameters) > 0 + else: + expect_args = len(inspect.getargspec(func)[0]) > 0 + self._func_ref = func_ref self._func_name = func_name self._func_path = func_path - self._ref_valid = True + self._expect_args = expect_args + self._ref_valid = func_ref is not None self._enabled = True - self._log = None - def __repr__(self): return "< {} - {} > {}".format( self.__class__.__name__, self._func_name, self._func_path @@ -83,7 +136,7 @@ class EventCallback(object): Args: event(Event): Event that was triggered. """ - self.log.info("Processing event {}".format(event.topic)) + # Skip if callback is not enabled or has invalid reference if not self._ref_valid or not self._enabled: return @@ -94,17 +147,15 @@ class EventCallback(object): if not callback: # Change state if is invalid so the callback is removed self._ref_valid = False - self.log.info("Invalid reference") elif self.topic_matches(event.topic): # Try execute callback - self.log.info("Triggering callback") - sig = inspect.signature(callback) try: - if len(sig.parameters) == 0: - callback() - else: + if self._expect_args: callback(event) + else: + callback() + except Exception: self.log.warning( "Failed to execute event callback {}".format( @@ -112,8 +163,6 @@ class EventCallback(object): ), exc_info=True ) - else: - self.log.info("Not matchin callback") # Inherit from 'object' for Python 2 hosts @@ -170,19 +219,7 @@ class StoredCallbacks: @classmethod def add_callback(cls, topic, callback): - # Convert callback into references - # - deleted functions won't cause crashes - if inspect.ismethod(callback): - ref = WeakMethod(callback) - elif callable(callback): - ref = weakref.ref(callback) - else: - print("Invalid callback") - return - - function_name = callback.__name__ - function_path = os.path.abspath(inspect.getfile(callback)) - callback = EventCallback(topic, ref, function_name, function_path) + callback = EventCallback(topic, callback) cls._registered_callbacks.append(callback) return callback From 17c88141c154003736e59e93f75bc405fc604845 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 12:20:50 +0100 Subject: [PATCH 05/27] fix emit of taskChanged topic --- openpype/lib/avalon_context.py | 4 ++-- openpype/lib/events.py | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 0bfd3f6de0..67a5515100 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( ) from .anatomy import Anatomy from .profiles_filtering import filter_profiles +from .events import emit_event # avalon module is not imported at the top # - may not be in path at the time of pype.lib initialization @@ -779,7 +780,6 @@ def update_current_task(task=None, asset=None, app=None, template_key=None): """ import avalon.api - from avalon.pipeline import emit changes = compute_session_changes( avalon.api.Session, @@ -799,7 +799,7 @@ def update_current_task(task=None, asset=None, app=None, template_key=None): os.environ[key] = value # Emit session change - emit("taskChanged", changes.copy()) + emit_event("taskChanged", changes.copy()) return changes diff --git a/openpype/lib/events.py b/openpype/lib/events.py index f71cb74c10..9496d71ca8 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -248,13 +248,30 @@ class StoredCallbacks: def register_event_callback(topic, callback): - """Add callback that will be executed on specific topic.""" + """Add callback that will be executed on specific topic. + + Args: + topic(str): Topic on which will callback be triggered. + callback(function): Callback that will be triggered when a topic + is triggered. Callback should expect none or 1 argument where + `Event` object is passed. + + Returns: + EventCallback: Object wrapping the callback. It can be used to + enable/disable listening to a topic or remove the callback from + the topic completely. + """ return StoredCallbacks.add_callback(topic, callback) def emit_event(topic, data=None, source=None): """Emit event with topic and data. + Arg: + topic(str): Event's topic. + data(dict): Event's additional data. Optional. + source(str): Who emitted the topic. Optional. + Returns: Event: Object of event that was emitted. """ From 38b6ad8042647c490c18fc8624e435b9218b83d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 12:21:53 +0100 Subject: [PATCH 06/27] Loader UI is using registering callback from init --- openpype/tools/loader/app.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index ec8f56e74b..d73a977ac6 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -26,17 +26,6 @@ module = sys.modules[__name__] module.window = None -# Register callback on task change -# - callback can't be defined in Window as it is weak reference callback -# so `WeakSet` will remove it immediately -def on_context_task_change(*args, **kwargs): - if module.window: - module.window.on_context_task_change(*args, **kwargs) - - -register_event_callback("taskChanged", on_context_task_change) - - class LoaderWindow(QtWidgets.QDialog): """Asset loader interface""" @@ -195,6 +184,8 @@ class LoaderWindow(QtWidgets.QDialog): self._first_show = True + register_event_callback("taskChanged", self.on_context_task_change) + def resizeEvent(self, event): super(LoaderWindow, self).resizeEvent(event) self._overlay_frame.resize(self.size()) From e0395fd0afadb39cd789e46cd5ae12eed7863ddc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 7 Mar 2022 12:42:43 +0100 Subject: [PATCH 07/27] hound fix --- openpype/lib/events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/events.py b/openpype/lib/events.py index 9496d71ca8..9cb80b2a6e 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -18,8 +18,8 @@ class EventCallback(object): may contain '*' that will be handled as "any characters". # Examples: - - "workfile.save" Callback will be triggered if the event topic is exactly - "workfile.save" . + - "workfile.save" Callback will be triggered if the event topic is + exactly "workfile.save" . - "workfile.*" Callback will be triggered an event topic starts with "workfile." so "workfile.save" and "workfile.open" will trigger the callback. From 319ec9e8684843e3562e3dc7680bbec35e919f94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 12:32:29 +0100 Subject: [PATCH 08/27] removed unused validate method --- openpype/lib/events.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openpype/lib/events.py b/openpype/lib/events.py index 9cb80b2a6e..f4ad82d919 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -223,18 +223,6 @@ class StoredCallbacks: cls._registered_callbacks.append(callback) return callback - @classmethod - def validate(cls): - invalid_callbacks = [] - for callbacks in cls._registered_callbacks: - for callback in tuple(callbacks): - callback.validate_ref() - if not callback.is_ref_valid: - invalid_callbacks.append(callback) - - for callback in invalid_callbacks: - cls._registered_callbacks.remove(callback) - @classmethod def emit_event(cls, event): invalid_callbacks = [] From 605e8dabb0467826ba160767c7102ebfedf828c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 12:33:29 +0100 Subject: [PATCH 09/27] modified docstring --- openpype/lib/events.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/lib/events.py b/openpype/lib/events.py index f4ad82d919..2ae3efa55d 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -169,13 +169,14 @@ class EventCallback(object): class Event(object): """Base event object. - Can be used to anything because data are not much specific. Only required - argument is topic which defines why event is happening and may be used for + Can be used for any event because is not specific. Only required argument + is topic which defines why event is happening and may be used for filtering. Arg: topic (str): Identifier of event. data (Any): Data specific for event. Dictionary is recommended. + source (str): Identifier of source. """ _data = {} From 42a185fef9008b58e28d2686ca02013afa719b87 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 12:35:59 +0100 Subject: [PATCH 10/27] change arguments for after.workfile.save event --- openpype/tools/workfiles/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 87e1492a20..df16498df5 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -859,7 +859,7 @@ class FilesWidget(QtWidgets.QWidget): # Trigger after save events emit_event( "after.workfile.save", - {"filepath": filepath}, + {"filename": work_filename, "workdir_path": self._workdir_path}, source="workfiles.tool" ) From 663d425635d63e89ffd1c893d03b88ef40b6af90 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 12:39:15 +0100 Subject: [PATCH 11/27] changed topics in workfiles tool to have before and after as last part --- openpype/hosts/maya/api/pipeline.py | 2 +- openpype/tools/workfiles/app.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 4945a9ba56..1dbcfbad6b 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -77,7 +77,7 @@ def install(): register_event_callback("new", on_new) register_event_callback("before.save", on_before_save) register_event_callback("taskChanged", on_task_changed) - register_event_callback("before.workfile.save", before_workfile_save) + register_event_callback("workfile.save.before", before_workfile_save) def _set_project(): diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index df16498df5..63958ac57b 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -824,7 +824,7 @@ class FilesWidget(QtWidgets.QWidget): # Trigger before save event emit_event( - "before.workfile.save", + "workfile.save.before", {"filename": work_filename, "workdir_path": self._workdir_path}, source="workfiles.tool" ) @@ -858,7 +858,7 @@ class FilesWidget(QtWidgets.QWidget): ) # Trigger after save events emit_event( - "after.workfile.save", + "workfile.save.after", {"filename": work_filename, "workdir_path": self._workdir_path}, source="workfiles.tool" ) From 2c75a20b340042c40ff924c6e24f5c7b8042653f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 12:42:17 +0100 Subject: [PATCH 12/27] added returncode in maya's 'before.save' --- openpype/hosts/maya/api/pipeline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 1dbcfbad6b..07743b7fc4 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -169,7 +169,10 @@ def _before_scene_save(return_code, client_data): # in order to block the operation. OpenMaya.MScriptUtil.setBool(return_code, True) - emit_event("before.save") + emit_event( + "before.save", + {"return_code": return_code} + ) def uninstall(): From c84abf1faa9606c70d8fc3e877e14e6276f09df1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 8 Mar 2022 12:50:11 +0100 Subject: [PATCH 13/27] raise TypeError when function is not callable --- openpype/lib/events.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/openpype/lib/events.py b/openpype/lib/events.py index 2ae3efa55d..7bec6ee30d 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -35,7 +35,11 @@ class EventCallback(object): Args: topic(str): Topic which will be listened. func(func): Callback to a topic. + + Raises: + TypeError: When passed function is not a callable object. """ + def __init__(self, topic, func): self._log = None self._topic = topic @@ -61,26 +65,21 @@ class EventCallback(object): elif callable(func): func_ref = weakref.ref(func) else: - func_ref = None - self.log.warning(( + raise TypeError(( "Registered callback is not callable. \"{}\"" ).format(str(func))) - func_name = None - func_path = None - expect_args = False # Collect additional data about function # - name # - path # - if expect argument or not - if func_ref is not None: - func_name = func.__name__ - func_path = os.path.abspath(inspect.getfile(func)) - if hasattr(inspect, "signature"): - sig = inspect.signature(func) - expect_args = len(sig.parameters) > 0 - else: - expect_args = len(inspect.getargspec(func)[0]) > 0 + func_name = func.__name__ + func_path = os.path.abspath(inspect.getfile(func)) + if hasattr(inspect, "signature"): + sig = inspect.signature(func) + expect_args = len(sig.parameters) > 0 + else: + expect_args = len(inspect.getargspec(func)[0]) > 0 self._func_ref = func_ref self._func_name = func_name From 342fa4c817c740eb8afb5d4c823595f9bd5eeaad Mon Sep 17 00:00:00 2001 From: joblet Date: Wed, 9 Mar 2022 17:50:06 +0100 Subject: [PATCH 14/27] fix phooshop and after effects openPype plugin path --- website/docs/artist_hosts_aftereffects.md | 2 +- website/docs/artist_hosts_photoshop.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index f9ef40fe1a..a9660bd13c 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -15,7 +15,7 @@ sidebar_label: AfterEffects ## Setup -To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}/repos/avalon-core/avalon/aftereffects/extension.zxp`. +To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select AfterEffects in menu. Then go to `{path to pype}hosts/aftereffects/api/extension.zxp`. Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. diff --git a/website/docs/artist_hosts_photoshop.md b/website/docs/artist_hosts_photoshop.md index 16539bcf79..b2b5fd58da 100644 --- a/website/docs/artist_hosts_photoshop.md +++ b/website/docs/artist_hosts_photoshop.md @@ -14,7 +14,7 @@ sidebar_label: Photoshop ## Setup -To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select Photoshop in menu. Then go to `{path to pype}/repos/avalon-core/avalon/photoshop/extension.zxp`. Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. +To install the extension, download, install [Anastasyi's Extension Manager](https://install.anastasiy.com/). Open Anastasyi's Extension Manager and select Photoshop in menu. Then go to `{path to pype}hosts/photoshop/api/extension.zxp`. Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension will install itself. ## Usage From c274e64c6aac478fac799df50b0cdc8c1d209fe0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 10 Mar 2022 09:47:39 +0100 Subject: [PATCH 15/27] fix houdini topic --- openpype/hosts/houdini/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index e34b8811b9..6cfb713661 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -108,7 +108,7 @@ def on_file_event_callback(event): elif event == hou.hipFileEventType.AfterSave: emit_event("save") elif event == hou.hipFileEventType.BeforeSave: - emit_event("before_save") + emit_event("before.save") elif event == hou.hipFileEventType.AfterClear: emit_event("new") From f3f781b43578dc1cef8cbbbf5c9276c5e48e7469 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 11 Mar 2022 11:03:15 +0100 Subject: [PATCH 16/27] added subset name filtering in ExtractReview --- openpype/plugins/publish/extract_review.py | 25 ++++++++++++++++--- .../defaults/project_settings/global.json | 3 ++- .../schemas/schema_global_publish.json | 9 +++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0b139a73e4..b8599454ee 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -103,9 +103,10 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("Matching profile: \"{}\"".format(json.dumps(profile))) + subset_name = instance.data.get("subset") instance_families = self.families_from_instance(instance) - filtered_outputs = self.filter_outputs_by_families( - profile, instance_families + filtered_outputs = self.filter_output_defs( + profile, subset_name, instance_families ) # Store `filename_suffix` to save arguments profile_outputs = [] @@ -1651,7 +1652,7 @@ class ExtractReview(pyblish.api.InstancePlugin): return True return False - def filter_outputs_by_families(self, profile, families): + def filter_output_defs(self, profile, subset_name, families): """Return outputs matching input instance families. Output definitions without families filter are marked as valid. @@ -1684,6 +1685,24 @@ class ExtractReview(pyblish.api.InstancePlugin): if not self.families_filter_validation(families, families_filters): continue + # Subsets name filters + subset_filters = [ + subset_filter + for subset_filter in output_filters.get("subsets", []) + # Skip empty strings + if subset_filter + ] + if subset_name and subset_filters: + match = False + for subset_filter in subset_filters: + compiled = re.compile(subset_filter) + if compiled.search(subset_name): + match = True + break + + if not match: + continue + filtered_outputs[filename_suffix] = output_def return filtered_outputs diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 9c44d9bc86..30a71b044a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -87,7 +87,8 @@ "render", "review", "ftrack" - ] + ], + "subsets": [] }, "overscan_crop": "", "overscan_color": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3eea7ccb30..12043d4205 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -291,6 +291,15 @@ "label": "Families", "type": "list", "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "subsets", + "label": "Subsets", + "type": "list", + "object_type": "text" } ] }, From 6706e90d1852f063670fcba5a1e630dde0c19a17 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Mar 2022 12:41:14 +0100 Subject: [PATCH 17/27] =?UTF-8?q?Fixes=20for=20=F0=9F=8E=AB=20OP-2825?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/api/plugin.py | 3 ++- .../hosts/maya/plugins/load/load_reference.py | 8 +------- .../plugins/publish/extract_maya_scene_raw.py | 18 +++++++----------- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index e0c21645e4..feaceacebc 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -178,7 +178,7 @@ class ReferenceLoader(Loader): loader=self.__class__.__name__ ) loaded_containers.append(container) - self._organize_containers([ref_node], container) + self._organize_containers(nodes, container) c += 1 namespace = None @@ -318,6 +318,7 @@ class ReferenceLoader(Loader): @staticmethod def _organize_containers(nodes, container): # type: (list, str) -> None + """Put containers in loaded data to correct hierarchy.""" for node in nodes: id_attr = "{}.id".format(node) if not cmds.attributeQuery("id", node=node, exists=True): diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 66cf95a643..d358c62724 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -121,18 +121,12 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): if family == "rig": self._post_process_rig(name, namespace, context, options) else: - if "translate" in options: cmds.setAttr(group_name + ".t", *options["translate"]) + print(new_nodes) return new_nodes - def load(self, context, name=None, namespace=None, options=None): - container = super(ReferenceLoader, self).load( - context, name, namespace, options) - # clean containers if present to AVALON_CONTAINERS - self._organize_containers(self[:], container[0]) - def switch(self, container, representation): self.update(container, representation) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index 5cc7b52090..2e1260c374 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -58,16 +58,12 @@ class ExtractMayaSceneRaw(openpype.api.Extractor): else: members = instance[:] - loaded_containers = None - if set(self.add_for_families).intersection( - set(instance.data.get("families")), - set(instance.data.get("family").lower())): - loaded_containers = self._add_loaded_containers(members) - selection = members - if loaded_containers: - self.log.info(loaded_containers) - selection += loaded_containers + if set(self.add_for_families).intersection( + set(instance.data.get("families"))) or \ + instance.data.get("family") in self.add_for_families: + selection += self._add_loaded_containers(members) + self.log.info(selection) # Perform extraction self.log.info("Performing extraction ...") @@ -105,7 +101,7 @@ class ExtractMayaSceneRaw(openpype.api.Extractor): if cmds.referenceQuery(ref, isNodeReferenced=True) ] - refs_to_include = set(refs_to_include) + members_with_refs = set(refs_to_include).union(members) obj_sets = cmds.ls("*.id", long=True, type="objectSet", recursive=True, objectsOnly=True) @@ -121,7 +117,7 @@ class ExtractMayaSceneRaw(openpype.api.Extractor): continue set_content = set(cmds.sets(obj_set, query=True)) - if set_content.intersection(refs_to_include): + if set_content.intersection(members_with_refs): loaded_containers.append(obj_set) return loaded_containers From e8b2299c00c5989430855faef2dd28005f5203ba Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Mar 2022 09:55:14 +0100 Subject: [PATCH 18/27] OP-2815 - copied webserver tool for AE from avalon to openpype --- .../hosts/aftereffects/api/launch_logic.py | 2 +- openpype/hosts/aftereffects/api/ws_stub.py | 2 +- openpype/tools/adobe_webserver/app.py | 237 ++++++++++++++++++ openpype/tools/adobe_webserver/readme.txt | 12 + 4 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 openpype/tools/adobe_webserver/app.py create mode 100644 openpype/tools/adobe_webserver/readme.txt diff --git a/openpype/hosts/aftereffects/api/launch_logic.py b/openpype/hosts/aftereffects/api/launch_logic.py index 97f14c9332..c549268978 100644 --- a/openpype/hosts/aftereffects/api/launch_logic.py +++ b/openpype/hosts/aftereffects/api/launch_logic.py @@ -15,7 +15,7 @@ from Qt import QtCore from openpype.tools.utils import host_tools from avalon import api -from avalon.tools.webserver.app import WebServerTool +from openpype.tools.adobe_webserver.app import WebServerTool from .ws_stub import AfterEffectsServerStub diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index 5a0600e92e..b0893310c1 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -8,7 +8,7 @@ import logging import attr from wsrpc_aiohttp import WebSocketAsync -from avalon.tools.webserver.app import WebServerTool +from openpype.tools.adobe_webserver.app import WebServerTool @attr.s diff --git a/openpype/tools/adobe_webserver/app.py b/openpype/tools/adobe_webserver/app.py new file mode 100644 index 0000000000..b79d6c6c60 --- /dev/null +++ b/openpype/tools/adobe_webserver/app.py @@ -0,0 +1,237 @@ +"""This Webserver tool is python 3 specific. + +Don't import directly to avalon.tools or implementation of Python 2 hosts +would break. +""" +import os +import logging +import urllib +import threading +import asyncio +import socket + +from aiohttp import web + +from wsrpc_aiohttp import ( + WSRPCClient +) + +from avalon import api + +log = logging.getLogger(__name__) + + +class WebServerTool: + """ + Basic POC implementation of asychronic websocket RPC server. + Uses class in external_app_1.py to mimic implementation for single + external application. + 'test_client' folder contains two test implementations of client + """ + _instance = None + + def __init__(self): + WebServerTool._instance = self + + self.client = None + self.handlers = {} + self.on_stop_callbacks = [] + + port = None + host_name = "localhost" + websocket_url = os.getenv("WEBSOCKET_URL") + if websocket_url: + parsed = urllib.parse.urlparse(websocket_url) + port = parsed.port + host_name = parsed.netloc.split(":")[0] + if not port: + port = 8098 # fallback + + self.port = port + self.host_name = host_name + + self.app = web.Application() + + # add route with multiple methods for single "external app" + self.webserver_thread = WebServerThread(self, self.port) + + def add_route(self, *args, **kwargs): + self.app.router.add_route(*args, **kwargs) + + def add_static(self, *args, **kwargs): + self.app.router.add_static(*args, **kwargs) + + def start_server(self): + if self.webserver_thread and not self.webserver_thread.is_alive(): + self.webserver_thread.start() + + def stop_server(self): + self.stop() + + async def send_context_change(self, host): + """ + Calls running webserver to inform about context change + + Used when new PS/AE should be triggered, + but one already running, without + this publish would point to old context. + """ + client = WSRPCClient(os.getenv("WEBSOCKET_URL"), + loop=asyncio.get_event_loop()) + await client.connect() + + project = api.Session["AVALON_PROJECT"] + asset = api.Session["AVALON_ASSET"] + task = api.Session["AVALON_TASK"] + log.info("Sending context change to {}-{}-{}".format(project, + asset, + task)) + + await client.call('{}.set_context'.format(host), + project=project, asset=asset, task=task) + await client.close() + + def port_occupied(self, host_name, port): + """ + Check if 'url' is already occupied. + + This could mean, that app is already running and we are trying open it + again. In that case, use existing running webserver. + Check here is easier than capturing exception from thread. + """ + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + result = True + try: + sock.bind((host_name, port)) + result = False + except: + print("Port is in use") + + return result + + def call(self, func): + log.debug("websocket.call {}".format(func)) + future = asyncio.run_coroutine_threadsafe( + func, + self.webserver_thread.loop + ) + result = future.result() + return result + + @staticmethod + def get_instance(): + if WebServerTool._instance is None: + WebServerTool() + return WebServerTool._instance + + @property + def is_running(self): + if not self.webserver_thread: + return False + return self.webserver_thread.is_running + + def stop(self): + if not self.is_running: + return + try: + log.debug("Stopping websocket server") + self.webserver_thread.is_running = False + self.webserver_thread.stop() + except Exception: + log.warning( + "Error has happened during Killing websocket server", + exc_info=True + ) + + def thread_stopped(self): + for callback in self.on_stop_callbacks: + callback() + + +class WebServerThread(threading.Thread): + """ Listener for websocket rpc requests. + + It would be probably better to "attach" this to main thread (as for + example Harmony needs to run something on main thread), but currently + it creates separate thread and separate asyncio event loop + """ + def __init__(self, module, port): + super(WebServerThread, self).__init__() + + self.is_running = False + self.port = port + self.module = module + self.loop = None + self.runner = None + self.site = None + self.tasks = [] + + def run(self): + self.is_running = True + + try: + log.info("Starting web server") + self.loop = asyncio.new_event_loop() # create new loop for thread + asyncio.set_event_loop(self.loop) + + self.loop.run_until_complete(self.start_server()) + + websocket_url = "ws://localhost:{}/ws".format(self.port) + + log.debug( + "Running Websocket server on URL: \"{}\"".format(websocket_url) + ) + + asyncio.ensure_future(self.check_shutdown(), loop=self.loop) + self.loop.run_forever() + except Exception: + self.is_running = False + log.warning( + "Websocket Server service has failed", exc_info=True + ) + raise + finally: + self.loop.close() # optional + + self.is_running = False + self.module.thread_stopped() + log.info("Websocket server stopped") + + async def start_server(self): + """ Starts runner and TCPsite """ + self.runner = web.AppRunner(self.module.app) + await self.runner.setup() + self.site = web.TCPSite(self.runner, 'localhost', self.port) + await self.site.start() + + def stop(self): + """Sets is_running flag to false, 'check_shutdown' shuts server down""" + self.is_running = False + + async def check_shutdown(self): + """ Future that is running and checks if server should be running + periodically. + """ + while self.is_running: + while self.tasks: + task = self.tasks.pop(0) + log.debug("waiting for task {}".format(task)) + await task + log.debug("returned value {}".format(task.result)) + + await asyncio.sleep(0.5) + + log.debug("Starting shutdown") + await self.site.stop() + log.debug("Site stopped") + await self.runner.cleanup() + log.debug("Runner stopped") + tasks = [task for task in asyncio.all_tasks() if + task is not asyncio.current_task()] + list(map(lambda task: task.cancel(), tasks)) # cancel all the tasks + results = await asyncio.gather(*tasks, return_exceptions=True) + log.debug(f'Finished awaiting cancelled tasks, results: {results}...') + await self.loop.shutdown_asyncgens() + # to really make sure everything else has time to stop + await asyncio.sleep(0.07) + self.loop.stop() diff --git a/openpype/tools/adobe_webserver/readme.txt b/openpype/tools/adobe_webserver/readme.txt new file mode 100644 index 0000000000..06cf140fc4 --- /dev/null +++ b/openpype/tools/adobe_webserver/readme.txt @@ -0,0 +1,12 @@ +Adobe webserver +--------------- +Aiohttp (Asyncio) based websocket server used for communication with host +applications, currently only for Adobe (but could be used for any non python +DCC which has websocket client). + +This webserver is started in spawned Python process that opens DCC during +its launch, waits for connection from DCC and handles communication going +forward. Server is closed before Python process is killed. + +(Different from `openpype/modules/webserver` as that one is running in Tray, +this one is running in spawn Python process.) \ No newline at end of file From 8a3be301414af9542ecb50b4424d8895d479ae4f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Mar 2022 14:31:00 +0100 Subject: [PATCH 19/27] OP-2815 - moved webserver tool to Openpype repo for PS --- openpype/hosts/photoshop/api/launch_logic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py index 112cd8fe3f..0021905cb5 100644 --- a/openpype/hosts/photoshop/api/launch_logic.py +++ b/openpype/hosts/photoshop/api/launch_logic.py @@ -14,7 +14,7 @@ from openpype.api import Logger from openpype.tools.utils import host_tools from avalon import api -from avalon.tools.webserver.app import WebServerTool +from openpype.tools.adobe_webserver.app import WebServerTool from .ws_stub import PhotoshopServerStub From 7c34eb7331f4e67dc4ac24f257c77602d2bbd0e5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 11 Mar 2022 14:43:26 +0100 Subject: [PATCH 20/27] OP-2815 - added missed import for PS --- openpype/hosts/photoshop/api/ws_stub.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py index d4406d17b9..64d89f5420 100644 --- a/openpype/hosts/photoshop/api/ws_stub.py +++ b/openpype/hosts/photoshop/api/ws_stub.py @@ -2,12 +2,11 @@ Stub handling connection from server to client. Used anywhere solution is calling client methods. """ -import sys import json import attr from wsrpc_aiohttp import WebSocketAsync -from avalon.tools.webserver.app import WebServerTool +from openpype.tools.adobe_webserver.app import WebServerTool @attr.s From c033eaad65afcb43752f9c5da85b94bab31bffb6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Mar 2022 15:13:43 +0100 Subject: [PATCH 21/27] fix update --- openpype/hosts/maya/api/plugin.py | 5 +++++ openpype/hosts/maya/plugins/load/load_reference.py | 2 -- .../hosts/maya/plugins/publish/extract_maya_scene_raw.py | 7 +++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index feaceacebc..1f90b3ffbd 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -247,6 +247,11 @@ class ReferenceLoader(Loader): self.log.warning("Ignoring file read error:\n%s", exc) + shapes = cmds.ls(content, shapes=True, long=True) + new_nodes = (list(set(content) - set(shapes))) + + self._organize_containers(new_nodes, container["objectName"]) + # Reapply alembic settings. if representation["name"] == "abc" and alembic_data: alembic_nodes = cmds.ls( diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index d358c62724..04a25f6493 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -123,8 +123,6 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): else: if "translate" in options: cmds.setAttr(group_name + ".t", *options["translate"]) - - print(new_nodes) return new_nodes def switch(self, container, representation): diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index 2e1260c374..9d73bea7d2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -60,10 +60,9 @@ class ExtractMayaSceneRaw(openpype.api.Extractor): selection = members if set(self.add_for_families).intersection( - set(instance.data.get("families"))) or \ + set(instance.data.get("families", []))) or \ instance.data.get("family") in self.add_for_families: - selection += self._add_loaded_containers(members) - self.log.info(selection) + selection += self._get_loaded_containers(members) # Perform extraction self.log.info("Performing extraction ...") @@ -93,7 +92,7 @@ class ExtractMayaSceneRaw(openpype.api.Extractor): self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) @staticmethod - def _add_loaded_containers(members): + def _get_loaded_containers(members): # type: (list) -> list refs_to_include = [ cmds.referenceQuery(ref, referenceNode=True) From 78d566b654074935141968e91429dc4be89510d6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 11 Mar 2022 16:40:41 +0100 Subject: [PATCH 22/27] replaced usage of avalon.lib.time with new function get_formatted_current_time --- .../hosts/harmony/plugins/publish/collect_farm_render.py | 3 ++- openpype/hosts/maya/plugins/publish/collect_render.py | 3 ++- openpype/hosts/maya/plugins/publish/collect_vrayscene.py | 3 ++- openpype/lib/__init__.py | 6 +++++- openpype/lib/abstract_collect_render.py | 2 +- openpype/lib/config.py | 6 ++++++ openpype/plugins/publish/collect_time.py | 6 +++--- 7 files changed, 21 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py index 85237094e4..35b123f97d 100644 --- a/openpype/hosts/harmony/plugins/publish/collect_farm_render.py +++ b/openpype/hosts/harmony/plugins/publish/collect_farm_render.py @@ -5,6 +5,7 @@ from pathlib import Path import attr from avalon import api +from openpype.lib import get_formatted_current_time import openpype.lib.abstract_collect_render import openpype.hosts.harmony.api as harmony from openpype.lib.abstract_collect_render import RenderInstance @@ -138,7 +139,7 @@ class CollectFarmRender(openpype.lib.abstract_collect_render. render_instance = HarmonyRenderInstance( version=version, - time=api.time(), + time=get_formatted_current_time(), source=context.data["currentFile"], label=node.split("/")[1], subset=subset_name, diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index d99e81573b..a525b562f3 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -50,6 +50,7 @@ import maya.app.renderSetup.model.renderSetup as renderSetup import pyblish.api from avalon import api +from openpype.lib import get_formatted_current_time from openpype.hosts.maya.api.lib_renderproducts import get as get_layer_render_products # noqa: E501 from openpype.hosts.maya.api import lib @@ -328,7 +329,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "family": "renderlayer", "families": ["renderlayer"], "asset": asset, - "time": api.time(), + "time": get_formatted_current_time(), "author": context.data["user"], # Add source to allow tracing back to the scene from # which was submitted originally diff --git a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py index c1e5d388af..327fc836dc 100644 --- a/openpype/hosts/maya/plugins/publish/collect_vrayscene.py +++ b/openpype/hosts/maya/plugins/publish/collect_vrayscene.py @@ -7,6 +7,7 @@ from maya import cmds import pyblish.api from avalon import api +from openpype.lib import get_formatted_current_time from openpype.hosts.maya.api import lib @@ -117,7 +118,7 @@ class CollectVrayScene(pyblish.api.InstancePlugin): "family": "vrayscene_layer", "families": ["vrayscene_layer"], "asset": api.Session["AVALON_ASSET"], - "time": api.time(), + "time": get_formatted_current_time(), "author": context.data["user"], # Add source to allow tracing back to the scene from # which was submitted originally diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 34b217f690..761ad3e9a0 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -63,7 +63,10 @@ from .anatomy import ( Anatomy ) -from .config import get_datetime_data +from .config import ( + get_datetime_data, + get_formatted_current_time +) from .python_module_tools import ( import_filepath, @@ -309,6 +312,7 @@ __all__ = [ "Anatomy", "get_datetime_data", + "get_formatted_current_time", "PypeLogger", "get_default_components", diff --git a/openpype/lib/abstract_collect_render.py b/openpype/lib/abstract_collect_render.py index 3839aad45d..7c768e280c 100644 --- a/openpype/lib/abstract_collect_render.py +++ b/openpype/lib/abstract_collect_render.py @@ -26,7 +26,7 @@ class RenderInstance(object): # metadata version = attr.ib() # instance version - time = attr.ib() # time of instance creation (avalon.api.time()) + time = attr.ib() # time of instance creation (get_formatted_current_time) source = attr.ib() # path to source scene file label = attr.ib() # label to show in GUI subset = attr.ib() # subset name diff --git a/openpype/lib/config.py b/openpype/lib/config.py index ba394cfd56..57e8efa57d 100644 --- a/openpype/lib/config.py +++ b/openpype/lib/config.py @@ -74,3 +74,9 @@ def get_datetime_data(datetime_obj=None): "S": str(int(seconds)), "SS": str(seconds), } + + +def get_formatted_current_time(): + return datetime.datetime.now().strftime( + "%Y%m%dT%H%M%SZ" + ) diff --git a/openpype/plugins/publish/collect_time.py b/openpype/plugins/publish/collect_time.py index e0adc7dfc3..7a005cc9cb 100644 --- a/openpype/plugins/publish/collect_time.py +++ b/openpype/plugins/publish/collect_time.py @@ -1,12 +1,12 @@ import pyblish.api -from avalon import api +from openpype.lib import get_formatted_current_time class CollectTime(pyblish.api.ContextPlugin): """Store global time at the time of publish""" label = "Collect Current Time" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.499 def process(self, context): - context.data["time"] = api.time() + context.data["time"] = get_formatted_current_time() From cf1453029f14dde78a18d8560ee55e69871bfe46 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Mar 2022 17:13:26 +0100 Subject: [PATCH 23/27] remove obsolete code and set cast --- openpype/hosts/maya/api/plugin.py | 6 +----- .../maya/plugins/publish/extract_maya_scene_raw.py | 12 ++++++------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 1f90b3ffbd..48d7c465ec 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -247,10 +247,7 @@ class ReferenceLoader(Loader): self.log.warning("Ignoring file read error:\n%s", exc) - shapes = cmds.ls(content, shapes=True, long=True) - new_nodes = (list(set(content) - set(shapes))) - - self._organize_containers(new_nodes, container["objectName"]) + self._organize_containers(content, container["objectName"]) # Reapply alembic settings. if representation["name"] == "abc" and alembic_data: @@ -289,7 +286,6 @@ class ReferenceLoader(Loader): to remove from scene. """ - from maya import cmds node = container["objectName"] diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index 9d73bea7d2..389995d30c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -94,13 +94,13 @@ class ExtractMayaSceneRaw(openpype.api.Extractor): @staticmethod def _get_loaded_containers(members): # type: (list) -> list - refs_to_include = [ - cmds.referenceQuery(ref, referenceNode=True) - for ref in members - if cmds.referenceQuery(ref, isNodeReferenced=True) - ] + refs_to_include = { + cmds.referenceQuery(node, referenceNode=True) + for node in members + if cmds.referenceQuery(node, isNodeReferenced=True) + } - members_with_refs = set(refs_to_include).union(members) + members_with_refs = refs_to_include.union(members) obj_sets = cmds.ls("*.id", long=True, type="objectSet", recursive=True, objectsOnly=True) From 3ef05cc480598ea1c900f48bd704c4d808647e3d Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 12 Mar 2022 03:37:54 +0000 Subject: [PATCH 24/27] [Automated] Bump version --- CHANGELOG.md | 10 +++++----- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f971c33208..ebc563c90b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,16 @@ # Changelog -## [3.9.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.0-nightly.9](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...HEAD) **Deprecated:** - AssetCreator: Remove the tool [\#2845](https://github.com/pypeclub/OpenPype/pull/2845) -- Houdini: Remove unused code [\#2779](https://github.com/pypeclub/OpenPype/pull/2779) **🚀 Enhancements** +- General: Subset name filtering in ExtractReview outpus [\#2872](https://github.com/pypeclub/OpenPype/pull/2872) - NewPublisher: Descriptions and Icons in creator dialog [\#2867](https://github.com/pypeclub/OpenPype/pull/2867) - NewPublisher: Changing task on publishing instance [\#2863](https://github.com/pypeclub/OpenPype/pull/2863) - TrayPublisher: Choose project widget is more clear [\#2859](https://github.com/pypeclub/OpenPype/pull/2859) @@ -22,10 +22,10 @@ - global: letter box calculated on output as last process [\#2812](https://github.com/pypeclub/OpenPype/pull/2812) - Nuke: adding Reformat to baking mov plugin [\#2811](https://github.com/pypeclub/OpenPype/pull/2811) - Manager: Update all to latest button [\#2805](https://github.com/pypeclub/OpenPype/pull/2805) -- General: Set context environments for non host applications [\#2803](https://github.com/pypeclub/OpenPype/pull/2803) **🐛 Bug fixes** +- General: Missing time function [\#2877](https://github.com/pypeclub/OpenPype/pull/2877) - Deadline: Fix plugin name for tile assemble [\#2868](https://github.com/pypeclub/OpenPype/pull/2868) - General: Fix hardlink for windows [\#2864](https://github.com/pypeclub/OpenPype/pull/2864) - General: ffmpeg was crashing on slate merge [\#2860](https://github.com/pypeclub/OpenPype/pull/2860) @@ -47,13 +47,13 @@ - Ftrack: Job killer with missing user [\#2819](https://github.com/pypeclub/OpenPype/pull/2819) - Nuke: Use AVALON\_APP to get value for "app" key [\#2818](https://github.com/pypeclub/OpenPype/pull/2818) - StandalonePublisher: use dynamic groups in subset names [\#2816](https://github.com/pypeclub/OpenPype/pull/2816) -- Settings UI: Search case sensitivity [\#2810](https://github.com/pypeclub/OpenPype/pull/2810) -- Flame Babypublisher optimalization [\#2806](https://github.com/pypeclub/OpenPype/pull/2806) **🔀 Refactored code** +- Refactor: move webserver tool to openpype [\#2876](https://github.com/pypeclub/OpenPype/pull/2876) - General: Move create logic from avalon to OpenPype [\#2854](https://github.com/pypeclub/OpenPype/pull/2854) - General: Add vendors from avalon [\#2848](https://github.com/pypeclub/OpenPype/pull/2848) +- General: Basic event system [\#2846](https://github.com/pypeclub/OpenPype/pull/2846) - General: Move change context functions [\#2839](https://github.com/pypeclub/OpenPype/pull/2839) - Tools: Don't use avalon tools code [\#2829](https://github.com/pypeclub/OpenPype/pull/2829) - Move Unreal Implementation to OpenPype [\#2823](https://github.com/pypeclub/OpenPype/pull/2823) diff --git a/openpype/version.py b/openpype/version.py index d4af8d760f..39d3037221 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.0-nightly.8" +__version__ = "3.9.0-nightly.9" diff --git a/pyproject.toml b/pyproject.toml index fe681266ca..7a7411fdfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.0-nightly.8" # OpenPype +version = "3.9.0-nightly.9" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 8d14f5fb7017adac8401137a6815ca49f1bd1a8b Mon Sep 17 00:00:00 2001 From: OpenPype Date: Mon, 14 Mar 2022 08:17:30 +0000 Subject: [PATCH 25/27] [Automated] Release --- CHANGELOG.md | 11 ++++------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc563c90b..5acb161bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.9.0-nightly.9](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.0](https://github.com/pypeclub/OpenPype/tree/3.9.0) (2022-03-14) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...3.9.0) **Deprecated:** @@ -27,6 +27,7 @@ - General: Missing time function [\#2877](https://github.com/pypeclub/OpenPype/pull/2877) - Deadline: Fix plugin name for tile assemble [\#2868](https://github.com/pypeclub/OpenPype/pull/2868) +- Nuke: gizmo precollect fix [\#2866](https://github.com/pypeclub/OpenPype/pull/2866) - General: Fix hardlink for windows [\#2864](https://github.com/pypeclub/OpenPype/pull/2864) - General: ffmpeg was crashing on slate merge [\#2860](https://github.com/pypeclub/OpenPype/pull/2860) - WebPublisher: Video file was published with one too many frame [\#2858](https://github.com/pypeclub/OpenPype/pull/2858) @@ -35,6 +36,7 @@ - Nuke: slate resolution to input video resolution [\#2853](https://github.com/pypeclub/OpenPype/pull/2853) - WebPublisher: Fix username stored in DB [\#2852](https://github.com/pypeclub/OpenPype/pull/2852) - WebPublisher: Fix wrong number of frames for video file [\#2851](https://github.com/pypeclub/OpenPype/pull/2851) +- Nuke: Fix family test in validate\_write\_legacy to work with stillImage [\#2847](https://github.com/pypeclub/OpenPype/pull/2847) - Nuke: fix multiple baking profile farm publishing [\#2842](https://github.com/pypeclub/OpenPype/pull/2842) - Blender: Fixed parameters for FBX export of the camera [\#2840](https://github.com/pypeclub/OpenPype/pull/2840) - Maya: Stop creation of reviews for Cryptomattes [\#2832](https://github.com/pypeclub/OpenPype/pull/2832) @@ -59,11 +61,6 @@ - Move Unreal Implementation to OpenPype [\#2823](https://github.com/pypeclub/OpenPype/pull/2823) - General: Extract template formatting from anatomy [\#2766](https://github.com/pypeclub/OpenPype/pull/2766) -**Merged pull requests:** - -- Nuke: gizmo precollect fix [\#2866](https://github.com/pypeclub/OpenPype/pull/2866) -- Nuke: Fix family test in validate\_write\_legacy to work with stillImage [\#2847](https://github.com/pypeclub/OpenPype/pull/2847) - ## [3.8.2](https://github.com/pypeclub/OpenPype/tree/3.8.2) (2022-02-07) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.8.2-nightly.3...3.8.2) diff --git a/openpype/version.py b/openpype/version.py index 39d3037221..d2182ac7da 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.0-nightly.9" +__version__ = "3.9.0" diff --git a/pyproject.toml b/pyproject.toml index 7a7411fdfd..681702560a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.0-nightly.9" # OpenPype +version = "3.9.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 3e840894137cbf40f9aff29e59cc4663c5dc29ec Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 14 Mar 2022 10:07:11 +0100 Subject: [PATCH 26/27] update avalon submodule --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index ffe9e910f1..7753d15507 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit ffe9e910f1f382e222d457d8e4a8426c41ed43ae +Subproject commit 7753d15507afadc143b7d49db8fcfaa6a29fed91 From 25c7e56613c78a69e22afe49b1c4fd6ea18adabc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 14 Mar 2022 11:16:02 +0100 Subject: [PATCH 27/27] move import of LegacyCreator to function --- openpype/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/__init__.py b/openpype/__init__.py index 7d046e4ef4..7720c9dfb8 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -5,7 +5,6 @@ import platform import functools import logging -from openpype.pipeline import LegacyCreator from .settings import get_project_settings from .lib import ( Anatomy, @@ -76,6 +75,7 @@ def install(): """Install Pype to Avalon.""" from pyblish.lib import MessageHandler from openpype.modules import load_modules + from openpype.pipeline import LegacyCreator from avalon import pipeline # Make sure modules are loaded