Merge branch 'develop' into enhancement/OP-2848_move-loader-logic-from-avalon-to-openpype

This commit is contained in:
Jakub Trllo 2022-03-14 11:47:33 +01:00
commit eb49761887
44 changed files with 720 additions and 211 deletions

View file

@ -1,16 +1,16 @@
# Changelog
## [3.9.0-nightly.8](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:**
- 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,11 +22,12 @@
- 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)
- 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)
@ -47,23 +49,18 @@
- 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)
- 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)

View file

@ -5,18 +5,13 @@ import platform
import functools
import logging
from openpype.pipeline import (
LegacyCreator,
register_loader_plugins_path,
deregister_loader_plugins_path,
)
from .settings import get_project_settings
from .lib import (
Anatomy,
filter_pyblish_plugins,
set_plugin_attributes_from_settings,
change_timer_to_current_context
change_timer_to_current_context,
register_event_callback,
)
pyblish = avalon = _original_discover = None
@ -80,6 +75,10 @@ def install():
"""Install Pype to Avalon."""
from pyblish.lib import MessageHandler
from openpype.modules import load_modules
from openpype.pipeline import (
LegacyCreator,
register_loader_plugins_path,
)
from avalon import pipeline
# Make sure modules are loaded
@ -133,16 +132,18 @@ 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()
@import_wrapper
def uninstall():
"""Uninstall Pype from Avalon."""
from openpype.pipeline import deregister_loader_plugins_path
log.info("Deregistering global plug-ins..")
pyblish.deregister_plugin_path(PUBLISH_PATH)
pyblish.deregister_discovery_filter(filter_pyblish_plugins)

View file

@ -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

View file

@ -15,6 +15,7 @@ from openpype.pipeline import (
deregister_loader_plugins_path,
)
import openpype.hosts.aftereffects
from openpype.lib import register_event_callback
from .launch_logic import get_stub
@ -78,7 +79,7 @@ def install():
"instanceToggled", on_pyblish_instance_toggled
)
avalon.api.on("application.launched", application_launch)
register_event_callback("application.launched", application_launch)
def uninstall():

View file

@ -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

View file

@ -20,6 +20,10 @@ from openpype.pipeline import (
deregister_loader_plugins_path,
)
from openpype.api import Logger
from openpype.lib import (
register_event_callback,
emit_event
)
import openpype.hosts.blender
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.blender.__file__))
@ -55,8 +59,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()
@ -118,22 +123,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
@ -141,9 +146,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()
@ -174,7 +179,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.
@ -191,7 +196,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'...")

View file

@ -9,6 +9,7 @@ import avalon.api
from avalon.pipeline import AVALON_CONTAINER_ID
from openpype import lib
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugins_path,
@ -134,7 +135,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.
@ -192,7 +193,7 @@ def install():
"instanceToggled", on_pyblish_instance_toggled
)
avalon.api.on("application.launched", application_launch)
register_event_callback("application.launched", application_launch)
def uninstall():

View file

@ -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,

View file

@ -1,12 +1,12 @@
import os
import hiero.core.events
import avalon.api as avalon
from openpype.api import Logger
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
@ -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'..")

View file

@ -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

View file

@ -19,7 +19,9 @@ 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
@ -55,11 +57,11 @@ def install():
avalon.api.register_plugin_path(LegacyCreator, 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
@ -105,13 +107,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():
@ -233,11 +235,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..")
@ -246,7 +248,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..")
@ -283,7 +285,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():

View file

@ -14,7 +14,11 @@ 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.lib import (
any_outdated,
register_event_callback,
emit_event
)
from openpype.lib.path_tools import HostDirmap
from openpype.pipeline import (
LegacyCreator,
@ -59,7 +63,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.
@ -73,12 +77,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("workfile.save.before", before_workfile_save)
def _set_project():
@ -141,7 +145,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 ...")
@ -152,15 +156,15 @@ def _on_maya_initialized(*args):
def _on_scene_new(*args):
avalon.api.emit("new", args)
emit_event("new")
def _on_scene_save(*args):
avalon.api.emit("save", args)
emit_event("save")
def _on_scene_open(*args):
avalon.api.emit("open", args)
emit_event("open")
def _before_scene_save(return_code, client_data):
@ -170,7 +174,10 @@ 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",
{"return_code": return_code}
)
def uninstall():
@ -347,7 +354,7 @@ def containerise(name,
return container
def on_init(_):
def on_init():
log.info("Running callback on init..")
def safe_deferred(fn):
@ -388,12 +395,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
@ -411,7 +418,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
@ -459,7 +466,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():
@ -475,7 +482,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()
@ -513,7 +520,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)

View file

@ -181,7 +181,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
@ -250,6 +250,8 @@ class ReferenceLoader(Loader):
self.log.warning("Ignoring file read error:\n%s", exc)
self._organize_containers(content, container["objectName"])
# Reapply alembic settings.
if representation["name"] == "abc" and alembic_data:
alembic_nodes = cmds.ls(
@ -287,7 +289,6 @@ class ReferenceLoader(Loader):
to remove from scene.
"""
from maya import cmds
node = container["objectName"]
@ -321,6 +322,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):

View file

@ -121,18 +121,10 @@ 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"])
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)

View file

@ -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

View file

@ -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

View file

@ -58,16 +58,11 @@ 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._get_loaded_containers(members)
# Perform extraction
self.log.info("Performing extraction ...")
@ -97,15 +92,15 @@ 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)
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)
}
refs_to_include = set(refs_to_include)
members_with_refs = refs_to_include.union(members)
obj_sets = cmds.ls("*.id", long=True, type="objectSet", recursive=True,
objectsOnly=True)
@ -121,7 +116,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

View file

@ -14,6 +14,7 @@ from openpype.api import (
BuildWorkfile,
get_current_project_settings
)
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugins_path,
@ -107,8 +108,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)
@ -231,7 +232,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)

View file

@ -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

View file

@ -6,6 +6,7 @@ import avalon.api
from avalon import pipeline, io
from openpype.api import Logger
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugins_path,
@ -79,7 +80,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():

View file

@ -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

View file

@ -21,7 +21,7 @@ from aiohttp_json_rpc.protocol import (
)
from aiohttp_json_rpc.exceptions import RpcError
from avalon import api
from openpype.lib 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)

View file

@ -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.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugins_path,
@ -89,8 +90,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():

View file

@ -14,10 +14,6 @@ PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
def application_launch():
pass
def install():
print("Installing Pype config...")
@ -25,7 +21,6 @@ def install():
log.info(PUBLISH_PATH)
io.install()
avalon.on("application.launched", application_launch)
def uninstall():

View file

@ -16,6 +16,11 @@ sys.path.insert(0, python_version_dir)
site.addsitedir(python_version_dir)
from .events import (
emit_event,
register_event_callback
)
from .vendor_bin_utils import (
find_executable,
get_vendor_bin_path,
@ -24,6 +29,7 @@ from .vendor_bin_utils import (
ffprobe_streams,
is_oiio_supported
)
from .env_tools import (
env_value_to_bool,
get_paths_from_environ,
@ -63,7 +69,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,
@ -194,6 +203,9 @@ from .openpype_version import (
terminal = Terminal
__all__ = [
"emit_event",
"register_event_callback",
"find_executable",
"get_openpype_execute_args",
"get_pype_execute_args",
@ -309,6 +321,7 @@ __all__ = [
"Anatomy",
"get_datetime_data",
"get_formatted_current_time",
"PypeLogger",
"get_default_components",

View file

@ -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

View file

@ -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

View file

@ -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"
)

268
openpype/lib/events.py Normal file
View file

@ -0,0 +1,268 @@
"""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 openpype.lib.python_2_comp import WeakMethod
class EventCallback(object):
"""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.
Raises:
TypeError: When passed function is not a callable object.
"""
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
# 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
# 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:
raise TypeError((
"Registered callback is not callable. \"{}\""
).format(str(func)))
# Collect additional data about function
# - name
# - path
# - if expect argument or not
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._expect_args = expect_args
self._ref_valid = func_ref is not None
self._enabled = True
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
try:
if self._expect_args:
callback(event)
else:
callback()
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 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 = {}
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):
callback = EventCallback(topic, callback)
cls._registered_callbacks.append(callback)
return callback
@classmethod
def emit_event(cls, event):
invalid_callbacks = []
for callback in cls._registered_callbacks:
callback.process_event(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.
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.
"""
event = Event(topic, data, source)
event.emit()
return event

View file

@ -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",

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -87,7 +87,8 @@
"render",
"review",
"ftrack"
]
],
"subsets": []
},
"overscan_crop": "",
"overscan_color": [

View file

@ -291,6 +291,15 @@
"label": "Families",
"type": "list",
"object_type": "text"
},
{
"type": "separator"
},
{
"key": "subsets",
"label": "Subsets",
"type": "list",
"object_type": "text"
}
]
},

View file

@ -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()

View file

@ -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.)

View file

@ -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.lib import register_event_callback
from openpype.tools.utils import (
lib,
PlaceholderLineEdit
@ -25,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)
pipeline.on("taskChanged", on_context_task_change)
class LoaderWindow(QtWidgets.QDialog):
"""Asset loader interface"""
@ -194,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())

View file

@ -9,10 +9,9 @@ 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.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,
@ -823,7 +823,11 @@ class FilesWidget(QtWidgets.QWidget):
return
# Trigger before save event
BeforeWorkfileSave.emit(work_filename, self._workdir_path)
emit_event(
"workfile.save.before",
{"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(
"workfile.save.after",
{"filename": work_filename, "workdir_path": self._workdir_path},
source="workfiles.tool"
)
self.workfile_created.emit(filepath)
# Refresh files model

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.9.0-nightly.8"
__version__ = "3.9.0"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.9.0-nightly.8" # OpenPype
version = "3.9.0" # OpenPype
description = "Open VFX and Animation pipeline with support."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"

@ -1 +1 @@
Subproject commit ffe9e910f1f382e222d457d8e4a8426c41ed43ae
Subproject commit 7753d15507afadc143b7d49db8fcfaa6a29fed91

View file

@ -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.

View file

@ -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