[Automated] Merged develop into main

This commit is contained in:
pypebot 2022-04-02 05:33:02 +02:00 committed by GitHub
commit 5ece99fe7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 2200 additions and 591 deletions

View file

@ -2,20 +2,16 @@
"""Pype module."""
import os
import platform
import functools
import logging
from .settings import get_project_settings
from .lib import (
Anatomy,
filter_pyblish_plugins,
set_plugin_attributes_from_settings,
change_timer_to_current_context,
register_event_callback,
)
pyblish = avalon = _original_discover = None
log = logging.getLogger(__name__)
@ -27,60 +23,17 @@ PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
def import_wrapper(func):
"""Wrap module imports to specific functions."""
@functools.wraps(func)
def decorated(*args, **kwargs):
global pyblish
global avalon
global _original_discover
if pyblish is None:
from pyblish import api as pyblish
from avalon import api as avalon
# we are monkey patching `avalon.api.discover()` to allow us to
# load plugin presets on plugins being discovered by avalon.
# Little bit of hacking, but it allows us to add out own features
# without need to modify upstream code.
_original_discover = avalon.discover
return func(*args, **kwargs)
return decorated
@import_wrapper
def patched_discover(superclass):
"""Patch `avalon.api.discover()`.
Monkey patched version of :func:`avalon.api.discover()`. It allows
us to load presets on plugins being discovered.
"""
# run original discover and get plugins
plugins = _original_discover(superclass)
filtered_plugins = [
plugin
for plugin in plugins
if issubclass(plugin, superclass)
]
set_plugin_attributes_from_settings(filtered_plugins, superclass)
return filtered_plugins
@import_wrapper
def install():
"""Install Pype to Avalon."""
"""Install OpenPype to Avalon."""
import avalon.api
import pyblish.api
from pyblish.lib import MessageHandler
from openpype.modules import load_modules
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_inventory_action,
register_creator_plugin_path,
)
from avalon import pipeline
# Make sure modules are loaded
load_modules()
@ -93,8 +46,8 @@ def install():
MessageHandler.emit = modified_emit
log.info("Registering global plug-ins..")
pyblish.register_plugin_path(PUBLISH_PATH)
pyblish.register_discovery_filter(filter_pyblish_plugins)
pyblish.api.register_plugin_path(PUBLISH_PATH)
pyblish.api.register_discovery_filter(filter_pyblish_plugins)
register_loader_plugin_path(LOAD_PATH)
project_name = os.environ.get("AVALON_PROJECT")
@ -103,7 +56,7 @@ def install():
if project_name:
anatomy = Anatomy(project_name)
anatomy.set_root_environments()
avalon.register_root(anatomy.roots)
avalon.api.register_root(anatomy.roots)
project_settings = get_project_settings(project_name)
platform_name = platform.system().lower()
@ -122,17 +75,14 @@ def install():
if not path or not os.path.exists(path):
continue
pyblish.register_plugin_path(path)
pyblish.api.register_plugin_path(path)
register_loader_plugin_path(path)
avalon.register_plugin_path(LegacyCreator, path)
register_creator_plugin_path(path)
register_inventory_action(path)
# apply monkey patched discover to original one
log.info("Patching discovery")
avalon.discover = patched_discover
pipeline.discover = patched_discover
register_event_callback("taskChanged", _on_task_change)
@ -140,16 +90,13 @@ def _on_task_change():
change_timer_to_current_context()
@import_wrapper
def uninstall():
"""Uninstall Pype from Avalon."""
import pyblish.api
from openpype.pipeline import deregister_loader_plugin_path
log.info("Deregistering global plug-ins..")
pyblish.deregister_plugin_path(PUBLISH_PATH)
pyblish.deregister_discovery_filter(filter_pyblish_plugins)
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
pyblish.api.deregister_discovery_filter(filter_pyblish_plugins)
deregister_loader_plugin_path(LOAD_PATH)
log.info("Global plug-ins unregistred")
# restore original discover
avalon.discover = _original_discover

View file

@ -5,15 +5,15 @@ from Qt import QtWidgets
from bson.objectid import ObjectId
import pyblish.api
import avalon.api
from avalon import io
from openpype import lib
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
import openpype.hosts.aftereffects
@ -73,7 +73,7 @@ def install():
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.api.register_callback(
@ -86,7 +86,7 @@ def install():
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def on_pyblish_instance_toggled(instance, old_value, new_value):

View file

@ -5,14 +5,6 @@ from openpype.pipeline import HOST_WORKFILE_EXTENSIONS
from .launch_logic import get_stub
def _active_document():
document_name = get_stub().get_active_document_name()
if not document_name:
return None
return document_name
def file_extensions():
return HOST_WORKFILE_EXTENSIONS["aftereffects"]
@ -39,7 +31,8 @@ def current_file():
full_name = get_stub().get_active_document_full_name()
if full_name and full_name != "null":
return os.path.normpath(full_name).replace("\\", "/")
except Exception:
except ValueError:
print("Nothing opened")
pass
return None
@ -47,3 +40,15 @@ def current_file():
def work_root(session):
return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/")
def _active_document():
# TODO merge with current_file - even in extension
document_name = None
try:
document_name = get_stub().get_active_document_name()
except ValueError:
print("Nothing opened")
pass
return document_name

View file

@ -1,12 +1,14 @@
from openpype.pipeline import create
from openpype.pipeline import CreatorError
from openpype.pipeline import (
CreatorError,
LegacyCreator
)
from openpype.hosts.aftereffects.api import (
get_stub,
list_instances
)
class CreateRender(create.LegacyCreator):
class CreateRender(LegacyCreator):
"""Render folder for publish.
Creates subsets in format 'familyTaskSubsetname',

View file

@ -29,12 +29,12 @@ def add_implementation_envs(env, _app):
env.get("OPENPYPE_BLENDER_USER_SCRIPTS") or ""
)
for path in openpype_blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
if path:
previous_user_scripts.add(os.path.normpath(path))
blender_user_scripts = env.get("BLENDER_USER_SCRIPTS") or ""
for path in blender_user_scripts.split(os.pathsep):
if path and os.path.exists(path):
if path:
previous_user_scripts.add(os.path.normpath(path))
# Remove implementation path from user script paths as is set to

View file

@ -14,9 +14,10 @@ import avalon.api
from avalon import io, schema
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.api import Logger
@ -54,7 +55,7 @@ def install():
pyblish.api.register_plugin_path(str(PUBLISH_PATH))
register_loader_plugin_path(str(LOAD_PATH))
avalon.api.register_plugin_path(LegacyCreator, str(CREATE_PATH))
register_creator_plugin_path(str(CREATE_PATH))
lib.append_user_scripts()
@ -76,7 +77,7 @@ def uninstall():
pyblish.api.deregister_plugin_path(str(PUBLISH_PATH))
deregister_loader_plugin_path(str(LOAD_PATH))
avalon.api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH))
deregister_creator_plugin_path(str(CREATE_PATH))
if not IS_HEADLESS:
ops.unregister()

View file

@ -18,6 +18,7 @@ log = Logger.get_logger(__name__)
FRAME_PATTERN = re.compile(r"[\._](\d+)[\.]")
class CTX:
# singleton used for passing data between api modules
app_framework = None
@ -538,9 +539,17 @@ def get_segment_attributes(segment):
# head and tail with forward compatibility
if segment.head:
clip_data["segment_head"] = int(segment.head)
# `infinite` can be also returned
if isinstance(segment.head, str):
clip_data["segment_head"] = 0
else:
clip_data["segment_head"] = int(segment.head)
if segment.tail:
clip_data["segment_tail"] = int(segment.tail)
# `infinite` can be also returned
if isinstance(segment.tail, str):
clip_data["segment_tail"] = 0
else:
clip_data["segment_tail"] = int(segment.tail)
# add all available shot tokens
shot_tokens = _get_shot_tokens_values(segment, [

View file

@ -3,14 +3,14 @@ Basic avalon integration
"""
import os
import contextlib
from avalon import api as avalon
from pyblish import api as pyblish
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from .lib import (
@ -37,7 +37,7 @@ def install():
pyblish.register_host("flame")
pyblish.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info("OpenPype Flame plug-ins registred ...")
# register callback for switching publishable
@ -52,7 +52,7 @@ def uninstall():
log.info("Deregistering Flame plug-ins..")
pyblish.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)

View file

@ -422,7 +422,13 @@ class WireTapCom(object):
color_policy = color_policy or "Legacy"
# check if the colour policy in custom dir
if not os.path.exists(color_policy):
if "/" in color_policy:
# if unlikelly full path was used make it redundant
color_policy = color_policy.replace("/syncolor/policies/", "")
# expecting input is `Shared/NameOfPolicy`
color_policy = "/syncolor/policies/{}".format(
color_policy)
else:
color_policy = "/syncolor/policies/Autodesk/{}".format(
color_policy)

View file

@ -34,119 +34,125 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
def process(self, context):
project = context.data["flameProject"]
sequence = context.data["flameSequence"]
selected_segments = context.data["flameSelectedSegments"]
self.log.debug("__ selected_segments: {}".format(selected_segments))
self.otio_timeline = context.data["otioTimeline"]
self.clips_in_reels = opfapi.get_clips_in_reels(project)
self.fps = context.data["fps"]
# process all sellected
with opfapi.maintained_segment_selection(sequence) as segments:
for segment in segments:
comment_attributes = self._get_comment_attributes(segment)
self.log.debug("_ comment_attributes: {}".format(
pformat(comment_attributes)))
for segment in selected_segments:
# get openpype tag data
marker_data = opfapi.get_segment_data_marker(segment)
self.log.debug("__ marker_data: {}".format(
pformat(marker_data)))
clip_data = opfapi.get_segment_attributes(segment)
clip_name = clip_data["segment_name"]
self.log.debug("clip_name: {}".format(clip_name))
if not marker_data:
continue
# get openpype tag data
marker_data = opfapi.get_segment_data_marker(segment)
self.log.debug("__ marker_data: {}".format(
pformat(marker_data)))
if marker_data.get("id") != "pyblish.avalon.instance":
continue
if not marker_data:
continue
self.log.debug("__ segment.name: {}".format(
segment.name
))
if marker_data.get("id") != "pyblish.avalon.instance":
continue
comment_attributes = self._get_comment_attributes(segment)
# get file path
file_path = clip_data["fpath"]
self.log.debug("_ comment_attributes: {}".format(
pformat(comment_attributes)))
# get source clip
source_clip = self._get_reel_clip(file_path)
clip_data = opfapi.get_segment_attributes(segment)
clip_name = clip_data["segment_name"]
self.log.debug("clip_name: {}".format(clip_name))
first_frame = opfapi.get_frame_from_filename(file_path) or 0
# get file path
file_path = clip_data["fpath"]
head, tail = self._get_head_tail(clip_data, first_frame)
# get source clip
source_clip = self._get_reel_clip(file_path)
# solve handles length
marker_data["handleStart"] = min(
marker_data["handleStart"], head)
marker_data["handleEnd"] = min(
marker_data["handleEnd"], tail)
first_frame = opfapi.get_frame_from_filename(file_path) or 0
with_audio = bool(marker_data.pop("audio"))
head, tail = self._get_head_tail(clip_data, first_frame)
# add marker data to instance data
inst_data = dict(marker_data.items())
# solve handles length
marker_data["handleStart"] = min(
marker_data["handleStart"], head)
marker_data["handleEnd"] = min(
marker_data["handleEnd"], tail)
asset = marker_data["asset"]
subset = marker_data["subset"]
with_audio = bool(marker_data.pop("audio"))
# insert family into families
family = marker_data["family"]
families = [str(f) for f in marker_data["families"]]
families.insert(0, str(family))
# add marker data to instance data
inst_data = dict(marker_data.items())
# form label
label = asset
if asset != clip_name:
label += " ({})".format(clip_name)
label += " {}".format(subset)
label += " {}".format("[" + ", ".join(families) + "]")
asset = marker_data["asset"]
subset = marker_data["subset"]
inst_data.update({
"name": "{}_{}".format(asset, subset),
"label": label,
"asset": asset,
"item": segment,
"families": families,
"publish": marker_data["publish"],
"fps": self.fps,
"flameSourceClip": source_clip,
"sourceFirstFrame": int(first_frame),
"path": file_path
})
# insert family into families
family = marker_data["family"]
families = [str(f) for f in marker_data["families"]]
families.insert(0, str(family))
# get otio clip data
otio_data = self._get_otio_clip_instance_data(clip_data) or {}
self.log.debug("__ otio_data: {}".format(pformat(otio_data)))
# form label
label = asset
if asset != clip_name:
label += " ({})".format(clip_name)
label += " {} [{}]".format(subset, ", ".join(families))
# add to instance data
inst_data.update(otio_data)
self.log.debug("__ inst_data: {}".format(pformat(inst_data)))
inst_data.update({
"name": "{}_{}".format(asset, subset),
"label": label,
"asset": asset,
"item": segment,
"families": families,
"publish": marker_data["publish"],
"fps": self.fps,
"flameSourceClip": source_clip,
"sourceFirstFrame": int(first_frame),
"path": file_path
})
# add resolution
self._get_resolution_to_data(inst_data, context)
# get otio clip data
otio_data = self._get_otio_clip_instance_data(clip_data) or {}
self.log.debug("__ otio_data: {}".format(pformat(otio_data)))
# add comment attributes if any
inst_data.update(comment_attributes)
# add to instance data
inst_data.update(otio_data)
self.log.debug("__ inst_data: {}".format(pformat(inst_data)))
# create instance
instance = context.create_instance(**inst_data)
# add resolution
self._get_resolution_to_data(inst_data, context)
# add colorspace data
instance.data.update({
"versionData": {
"colorspace": clip_data["colour_space"],
}
})
# add comment attributes if any
inst_data.update(comment_attributes)
# create shot instance for shot attributes create/update
self._create_shot_instance(context, clip_name, **inst_data)
# create instance
instance = context.create_instance(**inst_data)
self.log.info("Creating instance: {}".format(instance))
self.log.info(
"_ instance.data: {}".format(pformat(instance.data)))
# add colorspace data
instance.data.update({
"versionData": {
"colorspace": clip_data["colour_space"],
}
})
if not with_audio:
continue
# create shot instance for shot attributes create/update
self._create_shot_instance(context, clip_name, **inst_data)
# add audioReview attribute to plate instance data
# if reviewTrack is on
if marker_data.get("reviewTrack") is not None:
instance.data["reviewAudio"] = True
self.log.info("Creating instance: {}".format(instance))
self.log.info(
"_ instance.data: {}".format(pformat(instance.data)))
if not with_audio:
continue
# add audioReview attribute to plate instance data
# if reviewTrack is on
if marker_data.get("reviewTrack") is not None:
instance.data["reviewAudio"] = True
def _get_comment_attributes(self, segment):
comment = segment.comment.get_value()
@ -188,7 +194,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin):
# get pattern defined by type
pattern = TXT_PATERN
if a_type in ("number" , "float"):
if a_type in ("number", "float"):
pattern = NUM_PATERN
res_goup = pattern.findall(value)

View file

@ -31,27 +31,28 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin):
)
# adding otio timeline to context
with opfapi.maintained_segment_selection(sequence):
with opfapi.maintained_segment_selection(sequence) as selected_seg:
otio_timeline = flame_export.create_otio_timeline(sequence)
instance_data = {
"name": subset_name,
"asset": asset_doc["name"],
"subset": subset_name,
"family": "workfile"
}
instance_data = {
"name": subset_name,
"asset": asset_doc["name"],
"subset": subset_name,
"family": "workfile"
}
# create instance with workfile
instance = context.create_instance(**instance_data)
self.log.info("Creating instance: {}".format(instance))
# create instance with workfile
instance = context.create_instance(**instance_data)
self.log.info("Creating instance: {}".format(instance))
# update context with main project attributes
context.data.update({
"flameProject": project,
"flameSequence": sequence,
"otioTimeline": otio_timeline,
"currentFile": "Flame/{}/{}".format(
project.name, sequence.name
),
"fps": float(str(sequence.frame_rate)[:-4])
})
# update context with main project attributes
context.data.update({
"flameProject": project,
"flameSequence": sequence,
"otioTimeline": otio_timeline,
"currentFile": "Flame/{}/{}".format(
project.name, sequence.name
),
"flameSelectedSegments": selected_seg,
"fps": float(str(sequence.frame_rate)[:-4])
})

View file

@ -7,14 +7,14 @@ import logging
import contextlib
import pyblish.api
import avalon.api
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
deregister_loader_plugin_path,
register_creator_plugin_path,
register_inventory_action_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
deregister_inventory_action_path,
AVALON_CONTAINER_ID,
)
@ -70,7 +70,7 @@ def install():
log.info("Registering Fusion plug-ins..")
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
register_inventory_action_path(INVENTORY_PATH)
pyblish.api.register_callback(
@ -94,7 +94,7 @@ def uninstall():
log.info("Deregistering Fusion plug-ins..")
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
deregister_inventory_action_path(INVENTORY_PATH)
pyblish.api.deregister_callback(

View file

@ -1,13 +1,13 @@
import os
from openpype.pipeline import create
from openpype.pipeline import LegacyCreator
from openpype.hosts.fusion.api import (
get_current_comp,
comp_lock_and_undo_chunk
)
class CreateOpenEXRSaver(create.LegacyCreator):
class CreateOpenEXRSaver(LegacyCreator):
name = "openexrDefault"
label = "Create OpenEXR Saver"

View file

@ -6,14 +6,14 @@ from bson.objectid import ObjectId
import pyblish.api
from avalon import io
import avalon.api
from openpype import lib
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
import openpype.hosts.harmony
@ -108,9 +108,8 @@ def check_inventory():
if not lib.any_outdated():
return
host = avalon.api.registered_host()
outdated_containers = []
for container in host.ls():
for container in ls():
representation = container['representation']
representation_doc = io.find_one(
{
@ -186,7 +185,7 @@ def install():
pyblish.api.register_host("harmony")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
# Register callbacks.
@ -200,7 +199,7 @@ def install():
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def on_pyblish_instance_toggled(instance, old_value, new_value):

View file

@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
]
old_hiero_path = env.get("HIERO_PLUGIN_PATH") or ""
for path in old_hiero_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -5,13 +5,13 @@ import os
import contextlib
from collections import OrderedDict
from avalon import api as avalon
from avalon import schema
from pyblish import api as pyblish
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_creator_plugin_path,
register_loader_plugin_path,
deregister_creator_plugin_path,
deregister_loader_plugin_path,
AVALON_CONTAINER_ID,
)
@ -50,7 +50,7 @@ def install():
pyblish.register_host("hiero")
pyblish.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
@ -71,7 +71,7 @@ def uninstall():
pyblish.deregister_host("hiero")
pyblish.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)

View file

@ -15,7 +15,7 @@ def add_implementation_envs(env, _app):
old_houdini_menu_path = env.get("HOUDINI_MENU_PATH") or ""
for path in old_houdini_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)
@ -23,7 +23,7 @@ def add_implementation_envs(env, _app):
new_houdini_path.append(norm_path)
for path in old_houdini_menu_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -11,7 +11,7 @@ import avalon.api
from avalon.lib import find_submodule
from openpype.pipeline import (
LegacyCreator,
register_creator_plugin_path,
register_loader_plugin_path,
AVALON_CONTAINER_ID,
)
@ -54,7 +54,7 @@ def install():
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info("Installing callbacks ... ")
# register_event_callback("init", on_init)

View file

@ -9,7 +9,7 @@ def add_implementation_envs(env, _app):
]
old_python_path = env.get("PYTHONPATH") or ""
for path in old_python_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -23,8 +23,10 @@ from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_inventory_action_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_inventory_action_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.hosts.maya.lib import copy_workspace_mel
@ -60,7 +62,7 @@ def install():
pyblish.api.register_host("maya")
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
register_inventory_action_path(INVENTORY_PATH)
log.info(PUBLISH_PATH)
@ -189,7 +191,7 @@ def uninstall():
pyblish.api.deregister_host("maya")
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
deregister_inventory_action_path(INVENTORY_PATH)
menu.uninstall()

View file

@ -22,4 +22,6 @@ class CreateLook(plugin.Creator):
self.data["maketx"] = self.make_tx
# Enable users to force a copy.
# - on Windows is "forceCopy" always changed to `True` because of
# windows implementation of hardlinks
self.data["forceCopy"] = False

View file

@ -4,6 +4,7 @@ import os
import sys
import json
import tempfile
import platform
import contextlib
import subprocess
from collections import OrderedDict
@ -334,7 +335,14 @@ class ExtractLook(openpype.api.Extractor):
transfers = []
hardlinks = []
hashes = {}
force_copy = instance.data.get("forceCopy", False)
# Temporary fix to NOT create hardlinks on windows machines
if platform.system().lower() == "windows":
self.log.info(
"Forcing copy instead of hardlink due to issues on Windows..."
)
force_copy = True
else:
force_copy = instance.data.get("forceCopy", False)
for filepath in files_metadata:

View file

@ -10,7 +10,7 @@ def add_implementation_envs(env, _app):
]
old_nuke_path = env.get("NUKE_PATH") or ""
for path in old_nuke_path.split(os.pathsep):
if not path or not os.path.exists(path):
if not path:
continue
norm_path = os.path.normpath(path)

View file

@ -26,6 +26,7 @@ from openpype.tools.utils import host_tools
from openpype.lib.path_tools import HostDirmap
from openpype.settings import get_project_settings
from openpype.modules import ModulesManager
from openpype.pipeline import discover_legacy_creator_plugins
from .workio import (
save_file,
@ -1902,7 +1903,7 @@ def recreate_instance(origin_node, avalon_data=None):
# create new node
# get appropriate plugin class
creator_plugin = None
for Creator in api.discover(api.Creator):
for Creator in discover_legacy_creator_plugins():
if Creator.__name__ == data["creator"]:
creator_plugin = Creator
break

View file

@ -5,7 +5,6 @@ from collections import OrderedDict
import nuke
import pyblish.api
import avalon.api
import openpype
from openpype.api import (
@ -15,10 +14,11 @@ from openpype.api import (
)
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
register_inventory_action_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
deregister_inventory_action_path,
AVALON_CONTAINER_ID,
)
@ -106,7 +106,7 @@ def install():
log.info("Registering Nuke plug-ins..")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
register_inventory_action_path(INVENTORY_PATH)
# Register Avalon event for workfiles loading.
@ -132,7 +132,7 @@ def uninstall():
pyblish.deregister_host("nuke")
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
deregister_inventory_action_path(INVENTORY_PATH)
pyblish.api.deregister_callback(

View file

@ -450,6 +450,7 @@ class ExporterReviewMov(ExporterReview):
def generate_mov(self, farm=False, **kwargs):
self.publish_on_farm = farm
read_raw = kwargs["read_raw"]
reformat_node_add = kwargs["reformat_node_add"]
reformat_node_config = kwargs["reformat_node_config"]
bake_viewer_process = kwargs["bake_viewer_process"]
@ -484,6 +485,9 @@ class ExporterReviewMov(ExporterReview):
r_node["origlast"].setValue(self.last_frame)
r_node["colorspace"].setValue(self.write_colorspace)
if read_raw:
r_node["raw"].setValue(1)
# connect
self._temp_nodes[subset].append(r_node)
self.previous_node = r_node

View file

@ -0,0 +1,47 @@
import os
import pyblish.api
import openpype
from pprint import pformat
class ExtractReviewData(openpype.api.Extractor):
"""Extracts review tag into available representation
"""
order = pyblish.api.ExtractorOrder + 0.01
# order = pyblish.api.CollectorOrder + 0.499
label = "Extract Review Data"
families = ["review"]
hosts = ["nuke"]
def process(self, instance):
fpath = instance.data["path"]
ext = os.path.splitext(fpath)[-1][1:]
representations = instance.data.get("representations", [])
# review can be removed since `ProcessSubmittedJobOnFarm` will create
# reviable representation if needed
if (
"render.farm" in instance.data["families"]
and "review" in instance.data["families"]
):
instance.data["families"].remove("review")
# iterate representations and add `review` tag
for repre in representations:
if ext != repre["ext"]:
continue
if not repre.get("tags"):
repre["tags"] = []
if "review" not in repre["tags"]:
repre["tags"].append("review")
self.log.debug("Matching representation: {}".format(
pformat(repre)
))
instance.data["representations"] = representations

View file

@ -1,11 +1,10 @@
import os
import toml
import nuke
from avalon import api
import pyblish.api
import openpype.api
from openpype.pipeline import discover_creator_plugins
from openpype.hosts.nuke.api.lib import get_avalon_knob_data
@ -79,7 +78,7 @@ class ValidateWriteLegacy(pyblish.api.InstancePlugin):
# get appropriate plugin class
creator_plugin = None
for Creator in api.discover(api.Creator):
for Creator in discover_creator_plugins():
if Creator.__name__ != Create_name:
continue

View file

@ -9,9 +9,10 @@ from avalon import io
from openpype.api import Logger
from openpype.lib import register_event_callback
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
import openpype.hosts.photoshop
@ -75,7 +76,7 @@ def install():
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
log.info(PUBLISH_PATH)
pyblish.api.register_callback(
@ -88,7 +89,7 @@ def install():
def uninstall():
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def ls():

View file

@ -1,9 +1,9 @@
from Qt import QtWidgets
from openpype.pipeline import create
from openpype.pipeline import LegacyCreator
from openpype.hosts.photoshop import api as photoshop
class CreateImage(create.LegacyCreator):
class CreateImage(LegacyCreator):
"""Image folder for publish."""
name = "imageDefault"

View file

@ -1,6 +1,9 @@
from avalon import api
import pyblish.api
from openpype.settings import get_project_settings
from openpype.hosts.photoshop import api as photoshop
from openpype.lib import prepare_template_data
class CollectInstances(pyblish.api.ContextPlugin):
@ -9,6 +12,10 @@ class CollectInstances(pyblish.api.ContextPlugin):
This collector takes into account assets that are associated with
an LayerSet and marked with a unique identifier;
If no image instances are explicitly created, it looks if there is value
in `flatten_subset_template` (configurable in Settings), in that case it
produces flatten image with all visible layers.
Identifier:
id (str): "pyblish.avalon.instance"
"""
@ -19,13 +26,17 @@ class CollectInstances(pyblish.api.ContextPlugin):
families_mapping = {
"image": []
}
# configurable in Settings
flatten_subset_template = ""
def process(self, context):
stub = photoshop.stub()
layers = stub.get_layers()
layers_meta = stub.get_layers_metadata()
instance_names = []
all_layer_ids = []
for layer in layers:
all_layer_ids.append(layer.id)
layer_data = stub.read(layer, layers_meta)
# Skip layers without metadata.
@ -59,3 +70,33 @@ class CollectInstances(pyblish.api.ContextPlugin):
if len(instance_names) != len(set(instance_names)):
self.log.warning("Duplicate instances found. " +
"Remove unwanted via SubsetManager")
if len(instance_names) == 0 and self.flatten_subset_template:
project_name = context.data["projectEntity"]["name"]
variants = get_project_settings(project_name).get(
"photoshop", {}).get(
"create", {}).get(
"CreateImage", {}).get(
"defaults", [''])
family = "image"
task_name = api.Session["AVALON_TASK"]
asset_name = context.data["assetEntity"]["name"]
fill_pairs = {
"variant": variants[0],
"family": family,
"task": task_name
}
subset = self.flatten_subset_template.format(
**prepare_template_data(fill_pairs))
instance = context.create_instance(subset)
instance.data["family"] = family
instance.data["asset"] = asset_name
instance.data["subset"] = subset
instance.data["ids"] = all_layer_ids
instance.data["families"] = self.families_mapping[family]
instance.data["publish"] = True
self.log.info("flatten instance: {} ".format(instance.data))

View file

@ -2,18 +2,26 @@ import os
import pyblish.api
from openpype.lib import get_subset_name_with_asset_doc
class CollectReview(pyblish.api.ContextPlugin):
"""Gather the active document as review instance."""
label = "Review"
order = pyblish.api.CollectorOrder
order = pyblish.api.CollectorOrder + 0.1
hosts = ["photoshop"]
def process(self, context):
family = "review"
task = os.getenv("AVALON_TASK", None)
subset = family + task.capitalize()
subset = get_subset_name_with_asset_doc(
family,
"",
context.data["anatomyData"]["task"]["name"],
context.data["assetEntity"],
context.data["anatomyData"]["project"]["name"],
host_name=context.data["hostName"]
)
file_path = context.data["currentFile"]
base_name = os.path.basename(file_path)

View file

@ -1,6 +1,8 @@
import os
import pyblish.api
from openpype.lib import get_subset_name_with_asset_doc
class CollectWorkfile(pyblish.api.ContextPlugin):
"""Collect current script for publish."""
@ -11,8 +13,14 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
def process(self, context):
family = "workfile"
task = os.getenv("AVALON_TASK", None)
subset = family + task.capitalize()
subset = get_subset_name_with_asset_doc(
family,
"",
context.data["anatomyData"]["task"]["name"],
context.data["assetEntity"],
context.data["anatomyData"]["project"]["name"],
host_name=context.data["hostName"]
)
file_path = context.data["currentFile"]
staging_dir = os.path.dirname(file_path)

View file

@ -26,8 +26,10 @@ class ExtractImage(openpype.api.Extractor):
with photoshop.maintained_selection():
self.log.info("Extracting %s" % str(list(instance)))
with photoshop.maintained_visibility():
ids = set()
layer = instance.data.get("layer")
ids = set([layer.id])
if layer:
ids.add(layer.id)
add_ids = instance.data.pop("ids", None)
if add_ids:
ids.update(set(add_ids))

View file

@ -155,6 +155,9 @@ class ExtractReview(openpype.api.Extractor):
for image_instance in instance.context:
if image_instance.data["family"] != "image":
continue
if not image_instance.data.get("layer"):
# dummy instance for flatten image
continue
layers.append(image_instance.data.get("layer"))
return sorted(layers)

View file

@ -29,7 +29,8 @@ class ValidateNamingRepair(pyblish.api.Action):
stub = photoshop.stub()
for instance in instances:
self.log.info("validate_naming instance {}".format(instance))
metadata = stub.read(instance[0])
layer_item = instance.data["layer"]
metadata = stub.read(layer_item)
self.log.info("metadata instance {}".format(metadata))
layer_name = None
if metadata.get("uuid"):
@ -43,11 +44,11 @@ class ValidateNamingRepair(pyblish.api.Action):
stub.rename_layer(instance.data["uuid"], layer_name)
subset_name = re.sub(invalid_chars, replace_char,
instance.data["name"])
instance.data["subset"])
instance[0].Name = layer_name or subset_name
layer_item.name = layer_name or subset_name
metadata["subset"] = subset_name
stub.imprint(instance[0], metadata)
stub.imprint(layer_item, metadata)
return True

View file

@ -4,14 +4,17 @@ Basic avalon integration
import os
import contextlib
from collections import OrderedDict
from avalon import api as avalon
from avalon import schema
from pyblish import api as pyblish
from avalon import schema
from openpype.api import Logger
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from . import lib
@ -46,7 +49,7 @@ def install():
log.info("Registering DaVinci Resovle plug-ins..")
register_loader_plugin_path(LOAD_PATH)
avalon.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
@ -70,7 +73,7 @@ def uninstall():
log.info("Deregistering DaVinci Resovle plug-ins..")
deregister_loader_plugin_path(LOAD_PATH)
avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
# register callback for switching publishable
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)

View file

@ -30,7 +30,7 @@ class MyAutoCreator(AutoCreator):
def update_instances(self, update_list):
pipeline.update_instances(update_list)
def create(self, options=None):
def create(self):
existing_instance = None
for instance in self.create_context.instances:
if instance.family == self.family:

View file

@ -1,8 +1,8 @@
from openpype.hosts.traypublisher.api import pipeline
from openpype.lib import FileDef
from openpype.pipeline import (
Creator,
CreatedInstance,
lib
CreatedInstance
)
@ -80,7 +80,7 @@ class WorkfileCreator(Creator):
def get_instance_attr_defs(self):
output = [
lib.FileDef(
FileDef(
"filepath",
folders=False,
extensions=self.extensions,

View file

@ -6,7 +6,7 @@ from openpype.pipeline import PublishValidationError
class ValidateWorkfilePath(pyblish.api.InstancePlugin):
"""Validate existence of workfile instance existence."""
label = "Collect Workfile"
label = "Validate Workfile"
order = pyblish.api.ValidatorOrder - 0.49
families = ["workfile"]
hosts = ["traypublisher"]

View file

@ -15,9 +15,10 @@ 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_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
@ -82,7 +83,7 @@ def install():
pyblish.api.register_host("tvpaint")
pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH)
register_creator_plugin_path(CREATE_PATH)
registered_callbacks = (
pyblish.api.registered_callbacks().get("instanceToggled") or []
@ -104,7 +105,7 @@ def uninstall():
pyblish.api.deregister_host("tvpaint")
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
deregister_loader_plugin_path(LOAD_PATH)
avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH)
deregister_creator_plugin_path(CREATE_PATH)
def containerise(

View file

@ -20,21 +20,30 @@ class CollectInstances(pyblish.api.ContextPlugin):
json.dumps(workfile_instances, indent=4)
))
filtered_instance_data = []
# Backwards compatibility for workfiles that already have review
# instance in metadata.
review_instance_exist = False
for instance_data in workfile_instances:
if instance_data["family"] == "review":
family = instance_data["family"]
if family == "review":
review_instance_exist = True
break
elif family not in ("renderPass", "renderLayer"):
self.log.info("Unknown family \"{}\". Skipping {}".format(
family, json.dumps(instance_data, indent=4)
))
continue
filtered_instance_data.append(instance_data)
# Fake review instance if review was not found in metadata families
if not review_instance_exist:
workfile_instances.append(
filtered_instance_data.append(
self._create_review_instance_data(context)
)
for instance_data in workfile_instances:
for instance_data in filtered_instance_data:
instance_data["fps"] = context.data["sceneFps"]
# Store workfile instance data to instance data
@ -42,8 +51,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Global instance data modifications
# Fill families
family = instance_data["family"]
families = [family]
if family != "review":
families.append("review")
# Add `review` family for thumbnail integration
instance_data["families"] = [family, "review"]
instance_data["families"] = families
# Instance name
subset_name = instance_data["subset"]
@ -78,7 +90,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Project name from workfile context
project_name = context.data["workfile_context"]["project"]
# Host name from environment variable
host_name = os.environ["AVALON_APP"]
host_name = context.data["hostName"]
# Use empty variant value
variant = ""
task_name = io.Session["AVALON_TASK"]
@ -106,12 +118,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
instance = self.create_render_pass_instance(
context, instance_data
)
else:
raise AssertionError(
"Instance with unknown family \"{}\": {}".format(
family, instance_data
)
)
if instance is None:
continue

View file

@ -0,0 +1,110 @@
import json
import copy
import pyblish.api
from avalon import io
from openpype.lib import get_subset_name_with_asset_doc
class CollectRenderScene(pyblish.api.ContextPlugin):
"""Collect instance which renders whole scene in PNG.
Creates instance with family 'renderScene' which will have all layers
to render which will be composite into one result. The instance is not
collected from scene.
Scene will be rendered with all visible layers similar way like review is.
Instance is disabled if there are any created instances of 'renderLayer'
or 'renderPass'. That is because it is expected that this instance is
used as lazy publish of TVPaint file.
Subset name is created similar way like 'renderLayer' family. It can use
`renderPass` and `renderLayer` keys which can be set using settings and
`variant` is filled using `renderPass` value.
"""
label = "Collect Render Scene"
order = pyblish.api.CollectorOrder - 0.39
hosts = ["tvpaint"]
# Value of 'render_pass' in subset name template
render_pass = "beauty"
# Settings attributes
enabled = False
# Value of 'render_layer' and 'variant' in subset name template
render_layer = "Main"
def process(self, context):
# Check if there are created instances of renderPass and renderLayer
# - that will define if renderScene instance is enabled after
# collection
any_created_instance = False
for instance in context:
family = instance.data["family"]
if family in ("renderPass", "renderLayer"):
any_created_instance = True
break
# Global instance data modifications
# Fill families
family = "renderScene"
# Add `review` family for thumbnail integration
families = [family, "review"]
# Collect asset doc to get asset id
# - not sure if it's good idea to require asset id in
# get_subset_name?
workfile_context = context.data["workfile_context"]
asset_name = workfile_context["asset"]
asset_doc = io.find_one({
"type": "asset",
"name": asset_name
})
# Project name from workfile context
project_name = context.data["workfile_context"]["project"]
# Host name from environment variable
host_name = context.data["hostName"]
# Variant is using render pass name
variant = self.render_layer
dynamic_data = {
"render_layer": self.render_layer,
"render_pass": self.render_pass
}
task_name = workfile_context["task"]
subset_name = get_subset_name_with_asset_doc(
"render",
variant,
task_name,
asset_doc,
project_name,
host_name,
dynamic_data=dynamic_data
)
instance_data = {
"family": family,
"families": families,
"fps": context.data["sceneFps"],
"subset": subset_name,
"name": subset_name,
"label": "{} [{}-{}]".format(
subset_name,
context.data["sceneMarkIn"] + 1,
context.data["sceneMarkOut"] + 1
),
"active": not any_created_instance,
"publish": not any_created_instance,
"representations": [],
"layers": copy.deepcopy(context.data["layersData"]),
"asset": asset_name,
"task": task_name
}
instance = context.create_instance(**instance_data)
self.log.debug("Created instance: {}\n{}".format(
instance, json.dumps(instance.data, indent=4)
))

View file

@ -0,0 +1,99 @@
"""Plugin converting png files from ExtractSequence into exrs.
Requires:
ExtractSequence - source of PNG
ExtractReview - review was already created so we can convert to any exr
"""
import os
import json
import pyblish.api
from openpype.lib import (
get_oiio_tools_path,
run_subprocess,
)
from openpype.pipeline import KnownPublishError
class ExtractConvertToEXR(pyblish.api.InstancePlugin):
# Offset to get after ExtractSequence plugin.
order = pyblish.api.ExtractorOrder + 0.1
label = "Extract Sequence EXR"
hosts = ["tvpaint"]
families = ["render"]
enabled = False
# Replace source PNG files or just add
replace_pngs = True
# EXR compression
exr_compression = "ZIP"
def process(self, instance):
repres = instance.data.get("representations")
if not repres:
return
oiio_path = get_oiio_tools_path()
# Raise an exception when oiiotool is not available
# - this can currently happen on MacOS machines
if not os.path.exists(oiio_path):
KnownPublishError(
"OpenImageIO tool is not available on this machine."
)
new_repres = []
for repre in repres:
if repre["name"] != "png":
continue
self.log.info(
"Processing representation: {}".format(
json.dumps(repre, sort_keys=True, indent=4)
)
)
src_filepaths = set()
new_filenames = []
for src_filename in repre["files"]:
dst_filename = os.path.splitext(src_filename)[0] + ".exr"
new_filenames.append(dst_filename)
src_filepath = os.path.join(repre["stagingDir"], src_filename)
dst_filepath = os.path.join(repre["stagingDir"], dst_filename)
src_filepaths.add(src_filepath)
args = [
oiio_path, src_filepath,
"--compression", self.exr_compression,
# TODO how to define color conversion?
"--colorconvert", "sRGB", "linear",
"-o", dst_filepath
]
run_subprocess(args)
new_repres.append(
{
"name": "exr",
"ext": "exr",
"files": new_filenames,
"stagingDir": repre["stagingDir"],
"tags": list(repre["tags"])
}
)
if self.replace_pngs:
instance.data["representations"].remove(repre)
for filepath in src_filepaths:
instance.context.data["cleanupFullPaths"].append(filepath)
instance.data["representations"].extend(new_repres)
self.log.info(
"Representations: {}".format(
json.dumps(
instance.data["representations"], sort_keys=True, indent=4
)
)
)

View file

@ -12,14 +12,13 @@ from openpype.hosts.tvpaint.lib import (
fill_reference_frames,
composite_rendered_layers,
rename_filepaths_by_frame_start,
composite_images
)
class ExtractSequence(pyblish.api.Extractor):
label = "Extract Sequence"
hosts = ["tvpaint"]
families = ["review", "renderPass", "renderLayer"]
families = ["review", "renderPass", "renderLayer", "renderScene"]
# Modifiable with settings
review_bg = [255, 255, 255, 255]
@ -160,7 +159,7 @@ class ExtractSequence(pyblish.api.Extractor):
# Fill tags and new families
tags = []
if family_lowered in ("review", "renderlayer"):
if family_lowered in ("review", "renderlayer", "renderscene"):
tags.append("review")
# Sequence of one frame
@ -186,7 +185,7 @@ class ExtractSequence(pyblish.api.Extractor):
instance.data["representations"].append(new_repre)
if family_lowered in ("renderpass", "renderlayer"):
if family_lowered in ("renderpass", "renderlayer", "renderscene"):
# Change family to render
instance.data["family"] = "render"

View file

@ -8,7 +8,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin):
label = "Validate Layers Visibility"
order = pyblish.api.ValidatorOrder
families = ["review", "renderPass", "renderLayer"]
families = ["review", "renderPass", "renderLayer", "renderScene"]
def process(self, instance):
layer_names = set()

View file

@ -7,9 +7,10 @@ import pyblish.api
from avalon import api
from openpype.pipeline import (
LegacyCreator,
register_loader_plugin_path,
register_creator_plugin_path,
deregister_loader_plugin_path,
deregister_creator_plugin_path,
AVALON_CONTAINER_ID,
)
from openpype.tools.utils import host_tools
@ -49,7 +50,7 @@ def install():
logger.info("installing OpenPype for Unreal")
pyblish.api.register_plugin_path(str(PUBLISH_PATH))
register_loader_plugin_path(str(LOAD_PATH))
api.register_plugin_path(LegacyCreator, str(CREATE_PATH))
register_creator_plugin_path(str(CREATE_PATH))
_register_callbacks()
_register_events()
@ -58,7 +59,7 @@ def uninstall():
"""Uninstall Unreal configuration for Avalon."""
pyblish.api.deregister_plugin_path(str(PUBLISH_PATH))
deregister_loader_plugin_path(str(LOAD_PATH))
api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH))
deregister_creator_plugin_path(str(CREATE_PATH))
def _register_callbacks():

View file

@ -211,6 +211,7 @@ class ApplicationGroup:
data (dict): Group defying data loaded from settings.
manager (ApplicationManager): Manager that created the group.
"""
def __init__(self, name, data, manager):
self.name = name
self.manager = manager
@ -374,6 +375,7 @@ class ApplicationManager:
will always use these values. Gives ability to create manager
using different settings.
"""
def __init__(self, system_settings=None):
self.log = PypeLogger.get_logger(self.__class__.__name__)
@ -530,13 +532,13 @@ class EnvironmentToolGroup:
variants = data.get("variants") or {}
label_by_key = variants.pop(M_DYNAMIC_KEY_LABEL, {})
variants_by_name = {}
for variant_name, variant_env in variants.items():
for variant_name, variant_data in variants.items():
if variant_name in METADATA_KEYS:
continue
variant_label = label_by_key.get(variant_name) or variant_name
tool = EnvironmentTool(
variant_name, variant_label, variant_env, self
variant_name, variant_label, variant_data, self
)
variants_by_name[variant_name] = tool
self.variants = variants_by_name
@ -560,15 +562,30 @@ class EnvironmentTool:
Args:
name (str): Name of the tool.
environment (dict): Variant environments.
variant_data (dict): Variant data with environments and
host and app variant filters.
group (str): Name of group which wraps tool.
"""
def __init__(self, name, label, environment, group):
def __init__(self, name, label, variant_data, group):
# Backwards compatibility 3.9.1 - 3.9.2
# - 'variant_data' contained only environments but contain also host
# and application variant filters
host_names = variant_data.get("host_names", [])
app_variants = variant_data.get("app_variants", [])
if "environment" in variant_data:
environment = variant_data["environment"]
else:
environment = variant_data
self.host_names = host_names
self.app_variants = app_variants
self.name = name
self.variant_label = label
self.label = " ".join((group.label, label))
self.group = group
self._environment = environment
self.full_name = "/".join((group.name, name))
@ -579,6 +596,19 @@ class EnvironmentTool:
def environment(self):
return copy.deepcopy(self._environment)
def is_valid_for_app(self, app):
"""Is tool valid for application.
Args:
app (Application): Application for which are prepared environments.
"""
if self.app_variants and app.full_name not in self.app_variants:
return False
if self.host_names and app.host_name not in self.host_names:
return False
return True
class ApplicationExecutable:
"""Representation of executable loaded from settings."""
@ -1384,7 +1414,7 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True):
# Make sure each tool group can be added only once
for key in asset_doc["data"].get("tools_env") or []:
tool = app.manager.tools.get(key)
if not tool:
if not tool or not tool.is_valid_for_app(app):
continue
groups_by_name[tool.group.name] = tool.group
tool_by_group_name[tool.group.name][tool.name] = tool

View file

@ -1604,13 +1604,13 @@ def get_creator_by_name(creator_name, case_sensitive=False):
Returns:
Creator: Return first matching plugin or `None`.
"""
from openpype.pipeline import LegacyCreator
from openpype.pipeline import discover_legacy_creator_plugins
# Lower input creator name if is not case sensitive
if not case_sensitive:
creator_name = creator_name.lower()
for creator_plugin in avalon.api.discover(LegacyCreator):
for creator_plugin in discover_legacy_creator_plugins():
_creator_name = creator_plugin.__name__
# Lower creator plugin name if is not case sensitive
@ -1965,6 +1965,7 @@ def get_last_workfile(
data.pop("comment", None)
if not data.get("ext"):
data["ext"] = extensions[0]
data["ext"] = data["ext"].replace('.', '')
filename = StringTemplate.format_strict_template(file_template, data)
if full_path:

View file

@ -5,8 +5,9 @@ import importlib
import inspect
import logging
import six
log = logging.getLogger(__name__)
PY3 = sys.version_info[0] == 3
def import_filepath(filepath, module_name=None):
@ -28,7 +29,7 @@ def import_filepath(filepath, module_name=None):
# Prepare module object where content of file will be parsed
module = types.ModuleType(module_name)
if PY3:
if six.PY3:
# Use loader so module has full specs
module_loader = importlib.machinery.SourceFileLoader(
module_name, filepath
@ -38,7 +39,7 @@ def import_filepath(filepath, module_name=None):
# Execute module code and store content to module
with open(filepath) as _stream:
# Execute content and store it to module object
exec(_stream.read(), module.__dict__)
six.exec_(_stream.read(), module.__dict__)
module.__file__ = filepath
return module
@ -129,20 +130,12 @@ def classes_from_module(superclass, module):
for name in dir(module):
# It could be anything at this point
obj = getattr(module, name)
if not inspect.isclass(obj):
if not inspect.isclass(obj) or obj is superclass:
continue
# These are subclassed from nothing, not even `object`
if not len(obj.__bases__) > 0:
continue
if issubclass(obj, superclass):
classes.append(obj)
# Use string comparison rather than `issubclass`
# in order to support reloading of this module.
bases = recursive_bases_from_class(obj)
if not any(base.__name__ == superclass.__name__ for base in bases):
continue
classes.append(obj)
return classes
@ -228,7 +221,7 @@ def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None):
dst_module_name(str): Parent module name under which can be loaded
module added.
"""
if PY3:
if six.PY3:
module = _import_module_from_dirpath_py3(
dirpath, folder_name, dst_module_name
)

View file

@ -478,8 +478,14 @@ def convert_for_ffmpeg(
oiio_cmd.extend(["--eraseattrib", attr_name])
# Add last argument - path to output
base_file_name = os.path.basename(first_input_path)
output_path = os.path.join(output_dir, base_file_name)
if is_sequence:
ext = os.path.splitext(first_input_path)[1]
base_filename = "tmp.%{:0>2}d{}".format(
len(str(input_frame_end)), ext
)
else:
base_filename = os.path.basename(first_input_path)
output_path = os.path.join(output_dir, base_filename)
oiio_cmd.extend([
"-o", output_path
])

View file

@ -286,21 +286,6 @@ def from_dict_to_set(data, is_project):
return result
def get_avalon_project_template(project_name):
"""Get avalon template
Args:
project_name: (string)
Returns:
dictionary with templates
"""
templates = Anatomy(project_name).templates
return {
"workfile": templates["avalon"]["workfile"],
"work": templates["avalon"]["work"],
"publish": templates["avalon"]["publish"]
}
def get_project_apps(in_app_list):
""" Application definitions for app name.

View file

@ -27,11 +27,11 @@ class LogsWindow(QtWidgets.QWidget):
self.setStyleSheet(style.load_stylesheet())
self._frist_show = True
self._first_show = True
def showEvent(self, event):
super(LogsWindow, self).showEvent(event)
if self._frist_show:
self._frist_show = False
if self._first_show:
self._first_show = False
self.logs_widget.refresh()

View file

@ -13,6 +13,13 @@ from .create import (
LegacyCreator,
legacy_create,
discover_creator_plugins,
discover_legacy_creator_plugins,
register_creator_plugin,
deregister_creator_plugin,
register_creator_plugin_path,
deregister_creator_plugin_path,
)
from .load import (
@ -80,6 +87,13 @@ __all__ = (
"LegacyCreator",
"legacy_create",
"discover_creator_plugins",
"discover_legacy_creator_plugins",
"register_creator_plugin",
"deregister_creator_plugin",
"register_creator_plugin_path",
"deregister_creator_plugin_path",
# --- Load ---
"HeroVersionType",
"IncompatibleLoaderError",

View file

@ -1,4 +1,11 @@
import logging
from openpype.pipeline.plugin_discover import (
discover,
register_plugin,
register_plugin_path,
deregister_plugin,
deregister_plugin_path
)
class LauncherAction(object):
@ -90,28 +97,20 @@ class InventoryAction(object):
# Launcher action
def discover_launcher_actions():
import avalon.api
return avalon.api.discover(LauncherAction)
return discover(LauncherAction)
def register_launcher_action(plugin):
import avalon.api
return avalon.api.register_plugin(LauncherAction, plugin)
return register_plugin(LauncherAction, plugin)
def register_launcher_action_path(path):
import avalon.api
return avalon.api.register_plugin_path(LauncherAction, path)
return register_plugin_path(LauncherAction, path)
# Inventory action
def discover_inventory_actions():
import avalon.api
actions = avalon.api.discover(InventoryAction)
actions = discover(InventoryAction)
filtered_actions = []
for action in actions:
if action is not InventoryAction:
@ -121,24 +120,16 @@ def discover_inventory_actions():
def register_inventory_action(plugin):
import avalon.api
return avalon.api.register_plugin(InventoryAction, plugin)
return register_plugin(InventoryAction, plugin)
def deregister_inventory_action(plugin):
import avalon.api
avalon.api.deregister_plugin(InventoryAction, plugin)
deregister_plugin(InventoryAction, plugin)
def register_inventory_action_path(path):
import avalon.api
return avalon.api.register_plugin_path(InventoryAction, path)
return register_plugin_path(InventoryAction, path)
def deregister_inventory_action_path(path):
import avalon.api
return avalon.api.deregister_plugin_path(InventoryAction, path)
return deregister_plugin_path(InventoryAction, path)

View file

@ -6,7 +6,14 @@ from .creator_plugins import (
BaseCreator,
Creator,
AutoCreator
AutoCreator,
discover_creator_plugins,
discover_legacy_creator_plugins,
register_creator_plugin,
deregister_creator_plugin,
register_creator_plugin_path,
deregister_creator_plugin_path,
)
from .context import (
@ -29,6 +36,13 @@ __all__ = (
"Creator",
"AutoCreator",
"discover_creator_plugins",
"discover_legacy_creator_plugins",
"register_creator_plugin",
"deregister_creator_plugin",
"register_creator_plugin_path",
"deregister_creator_plugin_path",
"CreatedInstance",
"CreateContext",

View file

@ -9,7 +9,8 @@ from contextlib import contextmanager
from .creator_plugins import (
BaseCreator,
Creator,
AutoCreator
AutoCreator,
discover_creator_plugins,
)
from openpype.api import (
@ -17,6 +18,8 @@ from openpype.api import (
get_project_settings
)
UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"])
class ImmutableKeyError(TypeError):
"""Accessed key is immutable so does not allow changes or removements."""
@ -843,7 +846,7 @@ class CreateContext:
creators = {}
autocreators = {}
manual_creators = {}
for creator_class in avalon.api.discover(BaseCreator):
for creator_class in discover_creator_plugins():
if inspect.isabstract(creator_class):
self.log.info(
"Skipping abstract Creator {}".format(str(creator_class))
@ -1081,7 +1084,7 @@ class CreateContext:
for instance in cretor_instances:
instance_changes = instance.changes()
if instance_changes:
update_list.append((instance, instance_changes))
update_list.append(UpdateData(instance, instance_changes))
creator = self.creators[identifier]
if update_list:

View file

@ -8,7 +8,19 @@ from abc import (
)
import six
from openpype.lib import get_subset_name_with_asset_doc
from openpype.lib import (
get_subset_name_with_asset_doc,
set_plugin_attributes_from_settings,
)
from openpype.pipeline.plugin_discover import (
discover,
register_plugin,
register_plugin_path,
deregister_plugin,
deregister_plugin_path
)
from .legacy_create import LegacyCreator
class CreatorError(Exception):
@ -46,6 +58,11 @@ class BaseCreator:
# - may not be used if `get_icon` is reimplemented
icon = None
# Instance attribute definitions that can be changed per instance
# - returns list of attribute definitions from
# `openpype.pipeline.attribute_definitions`
instance_attr_defs = []
def __init__(
self, create_context, system_settings, project_settings, headless=False
):
@ -56,10 +73,13 @@ class BaseCreator:
# - we may use UI inside processing this attribute should be checked
self.headless = headless
@abstractproperty
@property
def identifier(self):
"""Identifier of creator (must be unique)."""
pass
"""Identifier of creator (must be unique).
Default implementation returns plugin's family.
"""
return self.family
@abstractproperty
def family(self):
@ -90,11 +110,39 @@ class BaseCreator:
pass
@abstractmethod
def collect_instances(self, attr_plugins=None):
def collect_instances(self):
"""Collect existing instances related to this creator plugin.
The implementation differs on host abilities. The creator has to
collect metadata about instance and create 'CreatedInstance' object
which should be added to 'CreateContext'.
Example:
```python
def collect_instances(self):
# Getting existing instances is different per host implementation
for instance_data in pipeline.list_instances():
# Process only instances that were created by this creator
creator_id = instance_data.get("creator_identifier")
if creator_id == self.identifier:
# Create instance object from existing data
instance = CreatedInstance.from_existing(
instance_data, self
)
# Add instance to create context
self._add_instance_to_context(instance)
```
"""
pass
@abstractmethod
def update_instances(self, update_list):
"""Store changes of existing instances so they can be recollected.
Args:
update_list(list<UpdateData>): Gets list of tuples. Each item
contain changed instance and it's changes.
"""
pass
@abstractmethod
@ -178,7 +226,7 @@ class BaseCreator:
list<AbtractAttrDef>: Attribute definitions that can be tweaked for
created instance.
"""
return []
return self.instance_attr_defs
class Creator(BaseCreator):
@ -191,6 +239,9 @@ class Creator(BaseCreator):
# - default_variants may not be used if `get_default_variants` is overriden
default_variants = []
# Default variant used in 'get_default_variant'
default_variant = None
# Short description of family
# - may not be used if `get_description` is overriden
description = None
@ -204,6 +255,10 @@ class Creator(BaseCreator):
# e.g. for buld creators
create_allow_context_change = True
# Precreate attribute definitions showed before creation
# - similar to instance attribute definitions
pre_create_attr_defs = []
@abstractmethod
def create(self, subset_name, instance_data, pre_create_data):
"""Create new instance and store it.
@ -263,7 +318,7 @@ class Creator(BaseCreator):
`get_default_variants` should be used.
"""
return None
return self.default_variant
def get_pre_create_attr_defs(self):
"""Plugin attribute definitions needed for creation.
@ -276,7 +331,7 @@ class Creator(BaseCreator):
list<AbtractAttrDef>: Attribute definitions that can be tweaked for
created instance.
"""
return []
return self.pre_create_attr_defs
class AutoCreator(BaseCreator):
@ -284,6 +339,43 @@ class AutoCreator(BaseCreator):
Can be used e.g. for `workfile`.
"""
def remove_instances(self, instances):
"""Skip removement."""
pass
def discover_creator_plugins():
return discover(BaseCreator)
def discover_legacy_creator_plugins():
plugins = discover(LegacyCreator)
set_plugin_attributes_from_settings(plugins, LegacyCreator)
return plugins
def register_creator_plugin(plugin):
if issubclass(plugin, BaseCreator):
register_plugin(BaseCreator, plugin)
elif issubclass(plugin, LegacyCreator):
register_plugin(LegacyCreator, plugin)
def deregister_creator_plugin(plugin):
if issubclass(plugin, BaseCreator):
deregister_plugin(BaseCreator, plugin)
elif issubclass(plugin, LegacyCreator):
deregister_plugin(LegacyCreator, plugin)
def register_creator_plugin_path(path):
register_plugin_path(BaseCreator, path)
register_plugin_path(LegacyCreator, path)
def deregister_creator_plugin_path(path):
deregister_plugin_path(BaseCreator, path)
deregister_plugin_path(LegacyCreator, path)

View file

@ -1,5 +1,13 @@
import logging
from openpype.lib import set_plugin_attributes_from_settings
from openpype.pipeline.plugin_discover import (
discover,
register_plugin,
register_plugin_path,
deregister_plugin,
deregister_plugin_path
)
from .utils import get_representation_path_from_context
@ -102,30 +110,22 @@ class SubsetLoaderPlugin(LoaderPlugin):
def discover_loader_plugins():
import avalon.api
return avalon.api.discover(LoaderPlugin)
plugins = discover(LoaderPlugin)
set_plugin_attributes_from_settings(plugins, LoaderPlugin)
return plugins
def register_loader_plugin(plugin):
import avalon.api
return avalon.api.register_plugin(LoaderPlugin, plugin)
def deregister_loader_plugin_path(path):
import avalon.api
avalon.api.deregister_plugin_path(LoaderPlugin, path)
def register_loader_plugin_path(path):
import avalon.api
return avalon.api.register_plugin_path(LoaderPlugin, path)
return register_plugin(LoaderPlugin, plugin)
def deregister_loader_plugin(plugin):
import avalon.api
deregister_plugin(LoaderPlugin, plugin)
avalon.api.deregister_plugin(LoaderPlugin, plugin)
def deregister_loader_plugin_path(path):
deregister_plugin_path(LoaderPlugin, path)
def register_loader_plugin_path(path):
return register_plugin_path(LoaderPlugin, path)

View file

@ -0,0 +1,298 @@
import os
import inspect
import traceback
from openpype.api import Logger
from openpype.lib.python_module_tools import (
modules_from_path,
classes_from_module,
)
log = Logger.get_logger(__name__)
class DiscoverResult:
"""Result of Plug-ins discovery of a single superclass type.
Stores discovered, duplicated, ignored and abstract plugins and file paths
which crashed on execution of file.
"""
def __init__(self, superclass):
self.superclass = superclass
self.plugins = []
self.crashed_file_paths = {}
self.duplicated_plugins = []
self.abstract_plugins = []
self.ignored_plugins = set()
# Store loaded modules to keep them in memory
self._modules = set()
def __iter__(self):
for plugin in self.plugins:
yield plugin
def __getitem__(self, item):
return self.plugins[item]
def __setitem__(self, item, value):
self.plugins[item] = value
def add_module(self, module):
"""Add dynamically loaded python module to keep it in memory."""
self._modules.add(module)
def get_report(self, only_errors=True, exc_info=True, full_report=False):
lines = []
if not only_errors:
# Successfully discovered plugins
if self.plugins or full_report:
lines.append(
"*** Discovered {} plugins".format(len(self.plugins))
)
for cls in self.plugins:
lines.append("- {}".format(cls.__class__.__name__))
# Plugin that were defined to be ignored
if self.ignored_plugins or full_report:
lines.append("*** Ignored plugins {}".format(len(
self.ignored_plugins
)))
for cls in self.ignored_plugins:
lines.append("- {}".format(cls.__class__.__name__))
# Abstract classes
if self.abstract_plugins or full_report:
lines.append("*** Discovered {} abstract plugins".format(len(
self.abstract_plugins
)))
for cls in self.abstract_plugins:
lines.append("- {}".format(cls.__class__.__name__))
# Abstract classes
if self.duplicated_plugins or full_report:
lines.append("*** There were {} duplicated plugins".format(len(
self.duplicated_plugins
)))
for cls in self.duplicated_plugins:
lines.append("- {}".format(cls.__class__.__name__))
if self.crashed_file_paths or full_report:
lines.append("*** Failed to load {} files".format(len(
self.crashed_file_paths
)))
for path, exc_info_args in self.crashed_file_paths.items():
lines.append("- {}".format(path))
if exc_info:
lines.append(10 * "*")
lines.extend(traceback.format_exception(*exc_info_args))
lines.append(10 * "*")
return "\n".join(lines)
def log_report(self, only_errors=True, exc_info=True):
report = self.get_report(only_errors, exc_info)
if report:
log.info(report)
class PluginDiscoverContext(object):
"""Store and discover registered types nad registered paths to types.
Keeps in memory all registered types and their paths. Paths are dynamically
loaded on discover so different discover calls won't return the same
class objects even if were loaded from same file.
"""
def __init__(self):
self._registered_plugins = {}
self._registered_plugin_paths = {}
self._last_discovered_plugins = {}
# Store the last result to memory
self._last_discovered_results = {}
def get_last_discovered_plugins(self, superclass):
"""Access last discovered plugin by a subperclass.
Returns:
None: When superclass was not discovered yet.
list: Lastly discovered plugins of the superclass.
"""
return self._last_discovered_plugins.get(superclass)
def discover(
self,
superclass,
allow_duplicates=True,
ignore_classes=None,
return_report=False
):
"""Find and return subclasses of `superclass`
Args:
superclass (type): Class which determines discovered subclasses.
allow_duplicates (bool): Validate class name duplications.
ignore_classes (list): List of classes that will be ignored
and not added to result.
Returns:
DiscoverResult: Object holding succesfully discovered plugins,
ignored plugins, plugins with missing abstract implementation
and duplicated plugin.
"""
if not ignore_classes:
ignore_classes = []
result = DiscoverResult(superclass)
plugin_names = set()
registered_classes = self._registered_plugins.get(superclass) or []
registered_paths = self._registered_plugin_paths.get(superclass) or []
for cls in registered_classes:
if cls is superclass or cls in ignore_classes:
result.ignored_plugins.add(cls)
continue
if inspect.isabstract(cls):
result.abstract_plugins.append(cls)
continue
class_name = cls.__name__
if class_name in plugin_names:
result.duplicated_plugins.append(cls)
continue
plugin_names.add(class_name)
result.plugins.append(cls)
# Include plug-ins from registered paths
for path in registered_paths:
modules, crashed = modules_from_path(path)
for item in crashed:
filepath, exc_info = item
result.crashed_file_paths[filepath] = exc_info
for item in modules:
filepath, module = item
result.add_module(module)
for cls in classes_from_module(superclass, module):
if cls is superclass or cls in ignore_classes:
result.ignored_plugins.add(cls)
continue
if inspect.isabstract(cls):
result.abstract_plugins.append(cls)
continue
if not allow_duplicates:
class_name = cls.__name__
if class_name in plugin_names:
result.duplicated_plugins.append(cls)
continue
plugin_names.add(class_name)
result.plugins.append(cls)
# Store in memory last result to keep in memory loaded modules
self._last_discovered_results[superclass] = result
self._last_discovered_plugins[superclass] = list(
result.plugins
)
result.log_report()
if return_report:
return result
return result.plugins
def register_plugin(self, superclass, cls):
"""Register a directory containing plug-ins of type `superclass`
Arguments:
superclass (type): Superclass of plug-in
cls (object): Subclass of `superclass`
"""
if superclass not in self._registered_plugins:
self._registered_plugins[superclass] = list()
if cls not in self._registered_plugins[superclass]:
self._registered_plugins[superclass].append(cls)
def register_plugin_path(self, superclass, path):
"""Register a directory of one or more plug-ins
Arguments:
superclass (type): Superclass of plug-ins to look for during
discovery
path (str): Absolute path to directory in which to discover
plug-ins
"""
if superclass not in self._registered_plugin_paths:
self._registered_plugin_paths[superclass] = list()
path = os.path.normpath(path)
if path not in self._registered_plugin_paths[superclass]:
self._registered_plugin_paths[superclass].append(path)
def registered_plugin_paths(self):
"""Return all currently registered plug-in paths"""
# Return shallow copy so we the original data can't be changed
return {
superclass: paths[:]
for superclass, paths in self._registered_plugin_paths.items()
}
def deregister_plugin(self, superclass, plugin):
"""Opposite of `register_plugin()`"""
if superclass in self._registered_plugins:
self._registered_plugins[superclass].remove(plugin)
def deregister_plugin_path(self, superclass, path):
"""Opposite of `register_plugin_path()`"""
self._registered_plugin_paths[superclass].remove(path)
class _GlobalDiscover:
"""Access to global object of PluginDiscoverContext.
Using singleton object to register/deregister plugins and plugin paths
and then discover them by superclass.
"""
_context = None
@classmethod
def get_context(cls):
if cls._context is None:
cls._context = PluginDiscoverContext()
return cls._context
def discover(superclass, allow_duplicates=True):
context = _GlobalDiscover.get_context()
return context.discover(superclass, allow_duplicates)
def get_last_discovered_plugins(superclass):
context = _GlobalDiscover.get_context()
return context.get_last_discovered_plugins(superclass)
def register_plugin(superclass, cls):
context = _GlobalDiscover.get_context()
context.register_plugin(superclass, cls)
def register_plugin_path(superclass, path):
context = _GlobalDiscover.get_context()
context.register_plugin_path(superclass, path)
def deregister_plugin(superclass, cls):
context = _GlobalDiscover.get_context()
context.deregister_plugin(superclass, cls)
def deregister_plugin_path(superclass, path):
context = _GlobalDiscover.get_context()
context.deregister_plugin_path(superclass, path)

View file

@ -2,6 +2,11 @@ import os
import copy
import logging
from .plugin_discover import (
discover,
register_plugin,
register_plugin_path,
)
log = logging.getLogger(__name__)
@ -126,21 +131,15 @@ class BinaryThumbnail(ThumbnailResolver):
# Thumbnail resolvers
def discover_thumbnail_resolvers():
import avalon.api
return avalon.api.discover(ThumbnailResolver)
return discover(ThumbnailResolver)
def register_thumbnail_resolver(plugin):
import avalon.api
return avalon.api.register_plugin(ThumbnailResolver, plugin)
register_plugin(ThumbnailResolver, plugin)
def register_thumbnail_resolver_path(path):
import avalon.api
return avalon.api.register_plugin_path(ThumbnailResolver, path)
register_plugin_path(ThumbnailResolver, path)
register_thumbnail_resolver(TemplateResolver)

View file

@ -0,0 +1,21 @@
"""
Requires:
None
Provides:
context
- cleanupFullPaths (list)
- cleanupEmptyDirs (list)
"""
import pyblish.api
class CollectCleanupKeys(pyblish.api.ContextPlugin):
"""Prepare keys for 'ExplicitCleanUp' plugin."""
label = "Collect Cleanup Keys"
order = pyblish.api.CollectorOrder
def process(self, context):
context.data["cleanupFullPaths"] = []
context.data["cleanupEmptyDirs"] = []

View file

@ -7,8 +7,12 @@ import shutil
from bson.objectid import ObjectId
from pymongo import InsertOne, ReplaceOne
import pyblish.api
from avalon import api, io, schema
from openpype.lib import create_hard_link
from openpype.lib import (
create_hard_link,
filter_profiles
)
class IntegrateHeroVersion(pyblish.api.InstancePlugin):
@ -17,7 +21,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
order = pyblish.api.IntegratorOrder + 0.1
optional = True
active = True
# Families are modified using settings
families = [
"model",
"rig",
@ -33,11 +39,13 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
"project", "asset", "task", "subset", "representation",
"family", "hierarchy", "task", "username"
]
# TODO add family filtering
# QUESTION/TODO this process should happen on server if crashed due to
# permissions error on files (files were used or user didn't have perms)
# *but all other plugins must be sucessfully completed
template_name_profiles = []
_default_template_name = "hero"
def process(self, instance):
self.log.debug(
"--- Integration of Hero version for subset `{}` begins.".format(
@ -51,27 +59,35 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
)
return
project_name = api.Session["AVALON_PROJECT"]
template_key = self._get_template_key(instance)
# TODO raise error if Hero not set?
anatomy = instance.context.data["anatomy"]
if "hero" not in anatomy.templates:
self.log.warning("!!! Anatomy does not have set `hero` key!")
return
if "path" not in anatomy.templates["hero"]:
project_name = api.Session["AVALON_PROJECT"]
if template_key not in anatomy.templates:
self.log.warning((
"!!! There is not set `path` template in `hero` anatomy"
" for project \"{}\"."
).format(project_name))
"!!! Anatomy of project \"{}\" does not have set"
" \"{}\" template key!"
).format(project_name, template_key))
return
hero_template = anatomy.templates["hero"]["path"]
if "path" not in anatomy.templates[template_key]:
self.log.warning((
"!!! There is not set \"path\" template in \"{}\" anatomy"
" for project \"{}\"."
).format(template_key, project_name))
return
hero_template = anatomy.templates[template_key]["path"]
self.log.debug("`hero` template check was successful. `{}`".format(
hero_template
))
hero_publish_dir = self.get_publish_dir(instance)
self.integrate_instance(instance, template_key, hero_template)
def integrate_instance(self, instance, template_key, hero_template):
anatomy = instance.context.data["anatomy"]
published_repres = instance.data["published_representations"]
hero_publish_dir = self.get_publish_dir(instance, template_key)
src_version_entity = instance.data.get("versionEntity")
filtered_repre_ids = []
@ -271,12 +287,12 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
continue
# Prepare anatomy data
anatomy_data = repre_info["anatomy_data"]
anatomy_data = copy.deepcopy(repre_info["anatomy_data"])
anatomy_data.pop("version", None)
# Get filled path to repre context
anatomy_filled = anatomy.format(anatomy_data)
template_filled = anatomy_filled["hero"]["path"]
template_filled = anatomy_filled[template_key]["path"]
repre_data = {
"path": str(template_filled),
@ -308,11 +324,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
collections, remainders = clique.assemble(published_files)
if remainders or not collections or len(collections) > 1:
raise Exception((
"Integrity error. Files of published representation "
"is combination of frame collections and single files."
"Collections: `{}` Single files: `{}`"
).format(str(collections),
str(remainders)))
"Integrity error. Files of published"
" representation is combination of frame"
" collections and single files. Collections:"
" `{}` Single files: `{}`"
).format(str(collections), str(remainders)))
src_col = collections[0]
@ -320,13 +336,10 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
frame_splitter = "_-_FRAME_SPLIT_-_"
anatomy_data["frame"] = frame_splitter
_anatomy_filled = anatomy.format(anatomy_data)
_template_filled = _anatomy_filled["hero"]["path"]
_template_filled = _anatomy_filled[template_key]["path"]
head, tail = _template_filled.split(frame_splitter)
padding = int(
anatomy.templates["render"].get(
"frame_padding",
anatomy.templates["render"].get("padding")
)
anatomy.templates[template_key]["frame_padding"]
)
dst_col = clique.Collection(
@ -444,6 +457,8 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
backup_hero_publish_dir is not None and
os.path.exists(backup_hero_publish_dir)
):
if os.path.exists(hero_publish_dir):
shutil.rmtree(hero_publish_dir)
os.rename(backup_hero_publish_dir, hero_publish_dir)
self.log.error((
"!!! Creating of hero version failed."
@ -466,13 +481,13 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
files.append(_path)
return files
def get_publish_dir(self, instance):
def get_publish_dir(self, instance, template_key):
anatomy = instance.context.data["anatomy"]
template_data = copy.deepcopy(instance.data["anatomyData"])
if "folder" in anatomy.templates["hero"]:
if "folder" in anatomy.templates[template_key]:
anatomy_filled = anatomy.format(template_data)
publish_folder = anatomy_filled["hero"]["folder"]
publish_folder = anatomy_filled[template_key]["folder"]
else:
# This is for cases of Deprecated anatomy without `folder`
# TODO remove when all clients have solved this issue
@ -489,7 +504,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
" key underneath `publish` (in global of for project `{}`)."
).format(project_name))
file_path = anatomy_filled["hero"]["path"]
file_path = anatomy_filled[template_key]["path"]
# Directory
publish_folder = os.path.dirname(file_path)
@ -499,6 +514,38 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
return publish_folder
def _get_template_key(self, instance):
anatomy_data = instance.data["anatomyData"]
task_data = anatomy_data.get("task") or {}
task_name = task_data.get("name")
task_type = task_data.get("type")
host_name = instance.context.data["hostName"]
# TODO raise error if Hero not set?
family = self.main_family_from_instance(instance)
key_values = {
"families": family,
"task_names": task_name,
"task_types": task_type,
"hosts": host_name
}
profile = filter_profiles(
self.template_name_profiles,
key_values,
logger=self.log
)
if profile:
template_name = profile["template_name"]
else:
template_name = self._default_template_name
return template_name
def main_family_from_instance(self, instance):
"""Returns main family of entered instance."""
family = instance.data.get("family")
if not family:
family = instance.data["families"][0]
return family
def copy_file(self, src_path, dst_path):
# TODO check drives if are the same to check if cas hardlink
dirname = os.path.dirname(dst_path)
@ -564,22 +611,16 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin):
src_file (string) - original file path
dst_file (string) - hero file path
"""
_, rootless = anatomy.find_root_template_from_path(
dst_file
)
_, rtls_src = anatomy.find_root_template_from_path(
src_file
)
_, rootless = anatomy.find_root_template_from_path(dst_file)
_, rtls_src = anatomy.find_root_template_from_path(src_file)
return path.replace(rtls_src, rootless)
def _update_hash(self, hash, src_file_name, dst_file):
"""
Updates hash value with proper hero name
"""
src_file_name = self._get_name_without_ext(
src_file_name)
hero_file_name = self._get_name_without_ext(
dst_file)
src_file_name = self._get_name_without_ext(src_file_name)
hero_file_name = self._get_name_without_ext(dst_file)
return hash.replace(src_file_name, hero_file_name)
def _get_name_without_ext(self, value):

View file

@ -8,11 +8,11 @@ M_ENVIRONMENT_KEY = "__environment_keys__"
# Metadata key for storing dynamic created labels
M_DYNAMIC_KEY_LABEL = "__dynamic_keys_labels__"
METADATA_KEYS = (
METADATA_KEYS = frozenset([
M_OVERRIDDEN_KEY,
M_ENVIRONMENT_KEY,
M_DYNAMIC_KEY_LABEL
)
])
# Keys where studio's system overrides are stored
GLOBAL_SETTINGS_KEY = "global_settings"

View file

@ -33,20 +33,6 @@
"enabled": false,
"profiles": []
},
"IntegrateHeroVersion": {
"enabled": true,
"optional": true,
"families": [
"model",
"rig",
"look",
"pointcache",
"animation",
"setdress",
"layout",
"mayaScene"
]
},
"ExtractJpegEXR": {
"enabled": true,
"ffmpeg_args": {
@ -204,6 +190,22 @@
}
]
},
"IntegrateHeroVersion": {
"enabled": true,
"optional": true,
"active": true,
"families": [
"model",
"rig",
"look",
"pointcache",
"animation",
"setdress",
"layout",
"mayaScene"
],
"template_name_profiles": []
},
"CleanUp": {
"paterns": [],
"remove_temp_renders": false

View file

@ -106,6 +106,9 @@
]
}
},
"ExtractReviewData": {
"enabled": false
},
"ExtractReviewDataLut": {
"enabled": false
},
@ -119,11 +122,10 @@
"families": [],
"sebsets": []
},
"extension": "mov",
"read_raw": false,
"viewer_process_override": "",
"bake_viewer_process": true,
"bake_viewer_input_process": true,
"add_tags": [],
"reformat_node_add": false,
"reformat_node_config": [
{
@ -151,7 +153,9 @@
"name": "pbb",
"value": false
}
]
],
"extension": "mov",
"add_tags": []
}
}
},

View file

@ -12,13 +12,16 @@
"flatten_subset_template": "",
"color_code_mapping": []
},
"CollectInstances": {
"flatten_subset_template": ""
},
"ValidateContainers": {
"enabled": true,
"optional": true,
"active": true
},
"ValidateNaming": {
"invalid_chars": "[ \\\\/+\\*\\?\\(\\)\\[\\]\\{\\}:,]",
"invalid_chars": "[ \\\\/+\\*\\?\\(\\)\\[\\]\\{\\}:,;]",
"replace_char": "_"
},
"ExtractImage": {
@ -44,4 +47,4 @@
"create_first_version": false,
"custom_templates": []
}
}
}

View file

@ -1,6 +1,10 @@
{
"stop_timer_on_application_exit": false,
"publish": {
"CollectRenderScene": {
"enabled": false,
"render_layer": "Main"
},
"ExtractSequence": {
"review_bg": [
255,
@ -28,6 +32,11 @@
"enabled": true,
"optional": true,
"active": true
},
"ExtractConvertToEXR": {
"enabled": false,
"replace_pngs": true,
"exr_compression": "ZIP"
}
},
"load": {

View file

@ -25,10 +25,18 @@
},
"variants": {
"3-2": {
"MTOA_VERSION": "3.2"
"host_names": [],
"app_variants": [],
"environment": {
"MTOA_VERSION": "3.2"
}
},
"3-1": {
"MTOA_VERSION": "3.1"
"host_names": [],
"app_variants": [],
"environment": {
"MTOA_VERSION": "3.1"
}
},
"__dynamic_keys_labels__": {
"3-2": "3.2",

View file

@ -42,7 +42,7 @@
"children": [
{
"type": "label",
"label": "Set color for publishable layers, set its resulting family and template for subset name. Can create flatten image from published instances"
"label": "Set color for publishable layers, set its resulting family and template for subset name. \nCan create flatten image from published instances.(Applicable only for remote publishing!)"
},
{
"type": "boolean",
@ -108,6 +108,23 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "CollectInstances",
"label": "Collect Instances",
"children": [
{
"type": "label",
"label": "Name for flatten image created if no image instance present"
},
{
"type": "text",
"key": "flatten_subset_template",
"label": "Subset template for flatten image"
}
]
},
{
"type": "schema_template",
"name": "template_publish_plugin",

View file

@ -16,6 +16,30 @@
"key": "publish",
"label": "Publish plugins",
"children": [
{
"type": "dict",
"collapsible": true,
"key": "CollectRenderScene",
"label": "Collect Render Scene",
"is_group": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "label",
"label": "It is possible to fill <b>'render_layer'</b> or <b>'variant'</b> in subset name template with custom value.<br/>- value of <b>'render_pass'</b> is always \"beauty\"."
},
{
"type": "text",
"key": "render_layer",
"label": "Render Layer"
}
]
},
{
"type": "dict",
"collapsible": true,
@ -78,6 +102,47 @@
"docstring": "Validate if shot on instances metadata is same as workfiles shot"
}
]
},
{
"type": "dict",
"key": "ExtractConvertToEXR",
"label": "Extract Convert To EXR",
"is_group": true,
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "label",
"label": "<b>WARNING:</b> This plugin does not work on MacOS (using OIIO tool)."
},
{
"type": "boolean",
"key": "replace_pngs",
"label": "Replace source PNG"
},
{
"type": "enum",
"key": "exr_compression",
"label": "EXR Compression",
"multiselection": false,
"enum_items": [
{"ZIP": "ZIP"},
{"ZIPS": "ZIPS"},
{"DWAA": "DWAA"},
{"DWAB": "DWAB"},
{"PIZ": "PIZ"},
{"RLE": "RLE"},
{"PXR24": "PXR24"},
{"B44": "B44"},
{"B44A": "B44A"},
{"none": "None"}
]
}
]
}
]
},

View file

@ -122,32 +122,6 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "IntegrateHeroVersion",
"label": "IntegrateHeroVersion",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "optional",
"label": "Optional"
},
{
"key": "families",
"label": "Families",
"type": "list",
"object_type": "text"
}
]
},
{
"type": "dict",
"collapsible": true,
@ -652,6 +626,80 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "IntegrateHeroVersion",
"label": "IntegrateHeroVersion",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "optional",
"label": "Optional"
},
{
"type": "boolean",
"key": "active",
"label": "Active"
},
{
"key": "families",
"label": "Families",
"type": "list",
"object_type": "text"
},
{
"type": "list",
"key": "template_name_profiles",
"label": "Template name profiles",
"use_label_wrap": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "families",
"label": "Families",
"type": "list",
"object_type": "text"
},
{
"type": "hosts-enum",
"key": "hosts",
"label": "Hosts",
"multiselection": true
},
{
"key": "task_types",
"label": "Task types",
"type": "task-types-enum"
},
{
"key": "task_names",
"label": "Task names",
"type": "list",
"object_type": "text"
},
{
"type": "separator"
},
{
"type": "text",
"key": "template_name",
"label": "Template name",
"tooltip": "Name of template from Anatomy templates"
}
]
}
}
]
},
{
"type": "dict",
"collapsible": true,

View file

@ -138,6 +138,21 @@
}
]
},
{
"type": "dict",
"collapsible": true,
"checkbox_key": "enabled",
"key": "ExtractReviewData",
"label": "ExtractReviewData",
"is_group": true,
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
}
]
},
{
"type": "dict",
"collapsible": true,
@ -208,9 +223,10 @@
"type": "separator"
},
{
"type": "text",
"key": "extension",
"label": "File extension"
"type": "boolean",
"key": "read_raw",
"label": "Read colorspace RAW",
"default": false
},
{
"type": "text",
@ -227,12 +243,6 @@
"key": "bake_viewer_input_process",
"label": "Bake Viewer Input Process (LUTs)"
},
{
"key": "add_tags",
"label": "Add additional tags to representations",
"type": "list",
"object_type": "text"
},
{
"type": "separator"
},
@ -246,7 +256,7 @@
"type": "collapsible-wrap",
"label": "Reformat Node Knobs",
"collapsible": true,
"collapsed": false,
"collapsed": true,
"children": [
{
"type": "list",
@ -347,6 +357,20 @@
}
}
]
},
{
"type": "separator"
},
{
"type": "text",
"key": "extension",
"label": "Write node file type"
},
{
"key": "add_tags",
"label": "Add additional tags to representations",
"type": "list",
"object_type": "text"
}
]
}

View file

@ -1,30 +0,0 @@
[
{
"type": "list-strict",
"key": "{name}",
"label": "{label}",
"object_types": [
{
"label": "Red",
"type": "number",
"minimum": 0,
"maximum": 1,
"decimal": 3
},
{
"label": "Green",
"type": "number",
"minimum": 0,
"maximum": 1,
"decimal": 3
},
{
"label": "Blue",
"type": "number",
"minimum": 0,
"maximum": 1,
"decimal": 3
}
]
}
]

View file

@ -25,7 +25,30 @@
"key": "variants",
"collapsible_key": true,
"object_type": {
"type": "raw-json"
"type": "dict",
"children": [
{
"key": "host_names",
"label": "Hosts",
"type": "hosts-enum",
"multiselection": true
},
{
"key": "app_variants",
"label": "Applications",
"type": "apps-enum",
"multiselection": true,
"tooltip": "Applications are not \"live\" and may require to Save and refresh settings UI to update values."
},
{
"type": "separator"
},
{
"key": "environment",
"label": "Environments",
"type": "raw-json"
}
]
}
}
]

View file

@ -265,11 +265,43 @@ def save_project_anatomy(project_name, anatomy_data):
raise SaveWarningExc(warnings)
def _system_settings_backwards_compatible_conversion(studio_overrides):
# Backwards compatibility of tools 3.9.1 - 3.9.2 to keep
# "tools" environments
if (
"tools" in studio_overrides
and "tool_groups" in studio_overrides["tools"]
):
tool_groups = studio_overrides["tools"]["tool_groups"]
for tool_group, group_value in tool_groups.items():
if tool_group in METADATA_KEYS:
continue
variants = group_value.get("variants")
if not variants:
continue
for key in set(variants.keys()):
if key in METADATA_KEYS:
continue
variant_value = variants[key]
if "environment" not in variant_value:
variants[key] = {
"environment": variant_value
}
@require_handler
def get_studio_system_settings_overrides(return_version=False):
return _SETTINGS_HANDLER.get_studio_system_settings_overrides(
output = _SETTINGS_HANDLER.get_studio_system_settings_overrides(
return_version
)
value = output
if return_version:
value, version = output
_system_settings_backwards_compatible_conversion(value)
return output
@require_handler

View file

@ -1,6 +1,10 @@
import avalon.api as api
import openpype
from openpype.pipeline import LegacyCreator
from openpype.pipeline import (
LegacyCreator,
register_creator_plugin,
discover_creator_plugins,
)
class MyTestCreator(LegacyCreator):
@ -27,8 +31,8 @@ def test_avalon_plugin_presets(monkeypatch, printer):
openpype.install()
api.register_host(Test())
api.register_plugin(LegacyCreator, MyTestCreator)
plugins = api.discover(LegacyCreator)
register_creator_plugin(MyTestCreator)
plugins = discover_creator_plugins()
printer("Test if we got our test plugin")
assert MyTestCreator in plugins
for p in plugins:

View file

@ -1,8 +1,7 @@
import uuid
from Qt import QtGui, QtCore
from avalon import api
from openpype.pipeline import LegacyCreator
from openpype.pipeline import discover_legacy_creator_plugins
from . constants import (
FAMILY_ROLE,
@ -22,7 +21,7 @@ class CreatorsModel(QtGui.QStandardItemModel):
self._creators_by_id = {}
items = []
creators = api.discover(LegacyCreator)
creators = discover_legacy_creator_plugins()
for creator in creators:
item_id = str(uuid.uuid4())
self._creators_by_id[item_id] = creator

View file

@ -271,7 +271,7 @@ class CreateDialog(QtWidgets.QDialog):
create_btn.setEnabled(False)
form_layout = QtWidgets.QFormLayout()
form_layout.addRow("Name:", variant_layout)
form_layout.addRow("Variant:", variant_layout)
form_layout.addRow("Subset:", subset_name_input)
mid_widget = QtWidgets.QWidget(self)

View file

@ -97,6 +97,9 @@ class CompleterView(QtWidgets.QListView):
QtCore.Qt.FramelessWindowHint
| QtCore.Qt.Tool
)
# Open the widget unactivated
self.setAttribute(QtCore.Qt.WA_ShowWithoutActivating)
delegate = QtWidgets.QStyledItemDelegate()
self.setItemDelegate(delegate)
@ -225,10 +228,18 @@ class SettingsLineEdit(PlaceholderLineEdit):
def __init__(self, *args, **kwargs):
super(SettingsLineEdit, self).__init__(*args, **kwargs)
self._completer = None
# Timer which will get started on focus in and stopped on focus out
# - callback checks if line edit or completer have focus
# and hide completer if not
focus_timer = QtCore.QTimer()
focus_timer.setInterval(50)
focus_timer.timeout.connect(self._on_focus_timer)
self.textChanged.connect(self._on_text_change)
self._completer = None
self._focus_timer = focus_timer
def _on_text_change(self, text):
if self._completer is not None:
self._completer.set_text_filter(text)
@ -240,19 +251,19 @@ class SettingsLineEdit(PlaceholderLineEdit):
new_point = self.mapToGlobal(point)
self._completer.move(new_point)
def _on_focus_timer(self):
if not self.hasFocus() and not self._completer.hasFocus():
self._completer.hide()
self._focus_timer.stop()
def focusInEvent(self, event):
super(SettingsLineEdit, self).focusInEvent(event)
self.focused_in.emit()
if self._completer is None:
return
self._completer.show()
self._update_completer()
def focusOutEvent(self, event):
super(SettingsLineEdit, self).focusOutEvent(event)
if self._completer is not None:
self._completer.hide()
self._focus_timer.start()
self._completer.show()
self._update_completer()
def paintEvent(self, event):
super(SettingsLineEdit, self).paintEvent(event)

View file

@ -17,6 +17,8 @@ from openpype.lib import filter_profiles
from openpype.style import get_objected_colors
from openpype.resources import get_image_path
log = Logger.get_logger(__name__)
def center_window(window):
"""Move window to center of it's screen."""
@ -111,13 +113,23 @@ def get_qta_icon_by_name_and_color(icon_name, icon_color):
variants.append("{0}.{1}".format(key, icon_name))
icon = None
used_variant = None
for variant in variants:
try:
icon = qtawesome.icon(variant, color=icon_color)
used_variant = variant
break
except Exception:
pass
if used_variant is None:
log.info("Didn't find icon \"{}\"".format(icon_name))
elif used_variant != icon_name:
log.debug("Icon \"{}\" was not found \"{}\" is used instead".format(
icon_name, used_variant
))
SharedObjects.icons[full_icon_name] = icon
return icon
@ -140,8 +152,8 @@ def get_asset_icon_name(asset_doc, has_children=True):
return icon_name
if has_children:
return "folder"
return "folder-o"
return "fa.folder"
return "fa.folder-o"
def get_asset_icon_color(asset_doc):

View file

@ -641,5 +641,6 @@ class SingleFileWidget(QtWidgets.QWidget):
filepaths.append(filepath)
# TODO filter check
if len(filepaths) == 1:
self.set_value(filepaths[0], False)
self._filepath_input.setText(filepaths[0])
event.accept()

84
poetry.lock generated
View file

@ -680,15 +680,8 @@ category = "main"
optional = false
python-versions = "*"
[package.dependencies]
attrs = ">=17.4.0"
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
pyrsistent = ">=0.14.0"
six = ">=1.11.0"
[package.extras]
format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"]
format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"]
format = ["rfc3987", "strict-rfc3339", "webcolors"]
[[package]]
name = "keyring"
@ -784,7 +777,7 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "paramiko"
version = "2.9.2"
version = "2.10.1"
description = "SSH2 protocol library"
category = "main"
optional = false
@ -794,6 +787,7 @@ python-versions = "*"
bcrypt = ">=3.1.3"
cryptography = ">=2.5"
pynacl = ">=1.0.1"
six = "*"
[package.extras]
all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"]
@ -1087,14 +1081,6 @@ category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "pyrsistent"
version = "0.18.1"
description = "Persistent/Functional/Immutable data structures"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "pysftp"
version = "0.2.9"
@ -1633,7 +1619,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[metadata]
lock-version = "1.1"
python-versions = "3.7.*"
content-hash = "2f78d48a6aad2d8a88b7dd7f31a76d907bec9fb65f0086fba6b6d2e1605f0f88"
content-hash = "b02313c8255a1897b0f0617ad4884a5943696c363512921aab1cb2dd8f4fdbe0"
[metadata.files]
acre = []
@ -2171,12 +2157,28 @@ log4mongo = [
{file = "log4mongo-1.7.0.tar.gz", hash = "sha256:dc374617206162a0b14167fbb5feac01dbef587539a235dadba6200362984a68"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
@ -2185,14 +2187,27 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
@ -2202,6 +2217,12 @@ markupsafe = [
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
@ -2277,8 +2298,8 @@ packaging = [
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
paramiko = [
{file = "paramiko-2.9.2-py2.py3-none-any.whl", hash = "sha256:04097dbd96871691cdb34c13db1883066b8a13a0df2afd4cb0a92221f51c2603"},
{file = "paramiko-2.9.2.tar.gz", hash = "sha256:944a9e5dbdd413ab6c7951ea46b0ab40713235a9c4c5ca81cfe45c6f14fa677b"},
{file = "paramiko-2.10.1-py2.py3-none-any.whl", hash = "sha256:f6cbd3e1204abfdbcd40b3ecbc9d32f04027cd3080fe666245e21e7540ccfc1b"},
{file = "paramiko-2.10.1.tar.gz", hash = "sha256:443f4da23ec24e9a9c0ea54017829c282abdda1d57110bf229360775ccd27a31"},
]
parso = [
{file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"},
@ -2598,29 +2619,6 @@ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pyrsistent = [
{file = "pyrsistent-0.18.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:df46c854f490f81210870e509818b729db4488e1f30f2a1ce1698b2295a878d1"},
{file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d45866ececf4a5fff8742c25722da6d4c9e180daa7b405dc0a2a2790d668c26"},
{file = "pyrsistent-0.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ed6784ceac462a7d6fcb7e9b663e93b9a6fb373b7f43594f9ff68875788e01e"},
{file = "pyrsistent-0.18.1-cp310-cp310-win32.whl", hash = "sha256:e4f3149fd5eb9b285d6bfb54d2e5173f6a116fe19172686797c056672689daf6"},
{file = "pyrsistent-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:636ce2dc235046ccd3d8c56a7ad54e99d5c1cd0ef07d9ae847306c91d11b5fec"},
{file = "pyrsistent-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e92a52c166426efbe0d1ec1332ee9119b6d32fc1f0bbfd55d5c1088070e7fc1b"},
{file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7a096646eab884bf8bed965bad63ea327e0d0c38989fc83c5ea7b8a87037bfc"},
{file = "pyrsistent-0.18.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdfd2c361b8a8e5d9499b9082b501c452ade8bbf42aef97ea04854f4a3f43b22"},
{file = "pyrsistent-0.18.1-cp37-cp37m-win32.whl", hash = "sha256:7ec335fc998faa4febe75cc5268a9eac0478b3f681602c1f27befaf2a1abe1d8"},
{file = "pyrsistent-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6455fc599df93d1f60e1c5c4fe471499f08d190d57eca040c0ea182301321286"},
{file = "pyrsistent-0.18.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6"},
{file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bfe2388663fd18bd8ce7db2c91c7400bf3e1a9e8bd7d63bf7e77d39051b85ec"},
{file = "pyrsistent-0.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e3e1fcc45199df76053026a51cc59ab2ea3fc7c094c6627e93b7b44cdae2c8c"},
{file = "pyrsistent-0.18.1-cp38-cp38-win32.whl", hash = "sha256:b568f35ad53a7b07ed9b1b2bae09eb15cdd671a5ba5d2c66caee40dbf91c68ca"},
{file = "pyrsistent-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1b96547410f76078eaf66d282ddca2e4baae8964364abb4f4dcdde855cd123a"},
{file = "pyrsistent-0.18.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5"},
{file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc66318fb7ee012071b2792024564973ecc80e9522842eb4e17743604b5e045"},
{file = "pyrsistent-0.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:914474c9f1d93080338ace89cb2acee74f4f666fb0424896fcfb8d86058bf17c"},
{file = "pyrsistent-0.18.1-cp39-cp39-win32.whl", hash = "sha256:1b34eedd6812bf4d33814fca1b66005805d3640ce53140ab8bbb1e2651b0d9bc"},
{file = "pyrsistent-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:e24a828f57e0c337c8d8bb9f6b12f09dfdf0273da25fda9e314f0b684b415a07"},
{file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"},
]
pysftp = [
{file = "pysftp-0.2.9.tar.gz", hash = "sha256:fbf55a802e74d663673400acd92d5373c1c7ee94d765b428d9f977567ac4854a"},
]

View file

@ -187,5 +187,6 @@ setup(
"build_dir": (openpype_root / "docs" / "build").as_posix()
}
},
executables=executables
executables=executables,
packages=[]
)

View file

@ -180,6 +180,9 @@ $out = & "$($env:POETRY_HOME)\bin\poetry" run python setup.py build 2>&1
Set-Content -Path "$($openpype_root)\build\build.log" -Value $out
if ($LASTEXITCODE -ne 0)
{
Write-Host "------------------------------------------" -ForegroundColor Red
Get-Content "$($openpype_root)\build\build.log"
Write-Host "------------------------------------------" -ForegroundColor Red
Write-Host "!!! " -NoNewLine -ForegroundColor Red
Write-Host "Build failed. Check the log: " -NoNewline
Write-Host ".\build\build.log" -ForegroundColor Yellow

View file

@ -185,9 +185,9 @@ if [ "$disable_submodule_update" == 1 ]; then
fi
echo -e "${BIGreen}>>>${RST} Building ..."
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
"$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" build &> "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; }
"$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" build &> "$openpype_root/build/build.log" || { echo -e "${BIRed}------------------------------------------${RST}"; cat "$openpype_root/build/build.log"; echo -e "${BIRed}------------------------------------------${RST}"; echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; }
elif [[ "$OSTYPE" == "darwin"* ]]; then
"$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac &> "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; }
"$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac &> "$openpype_root/build/build.log" || { echo -e "${BIRed}------------------------------------------${RST}"; cat "$openpype_root/build/build.log"; echo -e "${BIRed}------------------------------------------${RST}"; echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return 1; }
fi
"$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/build_dependencies.py"

View file

@ -49,6 +49,12 @@ With the `Creator` you have a variety of options to create:
- Uncheck `Use selection`.
- This will create a single group named after the `Subset` in the `Creator`.
#### Simplified publish
There is a simplified workflow for simple use case where only single image should be created containing all visible layers.
No image instances must be present in a workfile and `project_settings/photoshop/publish/CollectInstances/flatten_subset_template` must be filled in Settings.
Then artists just need to hit 'Publish' button in menu.
### Publish
When you are ready to share some work, you will need to publish. This is done by opening the `Pyblish` through the extensions `Publish` button.

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -0,0 +1,548 @@
---
id: dev_publishing
title: Publishing
sidebar_label: Publishing
toc_max_heading_level: 4
---
Publishing workflow consists of 2 parts:
- Creating - Mark what will be published and how.
- Publishing - Use data from Creating to go through the pyblish process.
OpenPype is using [pyblish](https://pyblish.com/) for the publishing process. OpenPype extends and modifies its few functions a bit, mainly for reports and UI purposes. The main differences are that OpenPype's publish UI allows to enable/disable instances or plugins during Creating part instead of in the publishing part and has limited plugin actions only for failed validation plugins.
## **Creating**
Concept of Creating does not have to "create" anything yet, but prepare and store metadata about an "instance" (becomes a subset after the publish process). Created instance always has `family` which defines what kind of data will be published, the best example is `workfile` family. Storing of metadata is host specific and may be even a Creator plugin specific. Most hosts are storing metadata into a workfile (Maya scene, Nuke script, etc.) to an item or a node the same way as regular Pyblish instances, so consistency of host implementation is kept, but some features may require a different approach that is the reason why it is creator plugin responsibility. Storing the metadata to the workfile persists values, so the artist does not have to create and set what should be published and how over and over.
### Created instance
Objected representation of created instance metadata defined by class **CreatedInstance**. Has access to **CreateContext** and **BaseCreator** that initialized the object. Is a dictionary-like object with few immutable keys (marked with start `*` in table). The immutable keys are set by the creator plugin or create context on initialization and their values can't change. Instance can have more arbitrary data, for example ids of nodes in scene but keep in mind that some keys are reserved.
| Key | Type | Description |
|---|---|---|
| *id | str | Identifier of metadata type. ATM constant **"pyblish.avalon.instance"** |
| *instance_id | str | Unique ID of instance. Set automatically on instance creation using `str(uuid.uuid4())` |
| *family | str | Instance's family representing type defined by creator plugin. |
| *creator_identifier | str | Identifier of creator that collected/created the instance. |
| *creator_attributes | dict | Dictionary of attributes that are defined by the creator plugin (`get_instance_attr_defs`). |
| *publish_attributes | dict | Dictionary of attributes that are defined by publish plugins. |
| variant | str | Variant is entered by the artist on creation and may affect **subset**. |
| subset | str | Name of instance. This name will be used as a subset name during publishing. Can be changed on context change or variant change. |
| active | bool | Is the instance active and will be published or not. |
| asset | str | Name of asset in which context was created. |
| task | str | Name of task in which context was created. Can be set to `None`. |
:::note
Task should not be required until the subset name template expects it.
:::
object of **CreatedInstance** has method **data_to_store** which returns a dictionary that can be parsed to a json string. This method will return all data related to the instance so it can be re-created using `CreatedInstance.from_existing(data)`.
#### *Create context* {#category-doc-link}
Controller and wrapper around Creating is `CreateContext` which cares about loading of plugins needed for Creating. And validates required functions in host implementation.
Context discovers creator and publish plugins. Trigger collections of existing instances on creators and trigger Creating itself. Also it keeps in mind instance objects by their ids.
Creator plugins can call **creator_adds_instance** or **creator_removed_instance** to add/remove instances but these methods are not meant to be called directly out of the creator. The reason is that it is the creator's responsibility to remove metadata or decide if it should remove the instance.
#### Required functions in host implementation
Host implementation **must** implement **get_context_data** and **update_context_data**. These two functions are needed to store metadata that are not related to any instance but are needed for Creating and publishing process. Right now only data about enabled/disabled optional publish plugins is stored there. When data is not stored and loaded properly, reset of publishing will cause that they will be set to default value. Context data also parsed to json string similarly as instance data.
There are also few optional functions. For UI purposes it is possible to implement **get_context_title** which can return a string shown in UI as a title. Output string may contain html tags. It is recommended to return context path (it will be created function this purposes) in this order `"{project name}/{asset hierarchy}/<b>{asset name}</b>/{task name}"`.
Another optional function is **get_current_context**. This function is handy in hosts where it is possible to open multiple workfiles in one process so using global context variables is not relevant because artists can switch between opened workfiles without being acknowledged. When a function is not implemented or won't return the right keys the global context is used.
```json
# Expected keys in output
{
"project_name": "MyProject",
"asset_name": "sq01_sh0010",
"task_name": "Modeling"
}
```
### Create plugin
Main responsibility of create plugin is to create, update, collect and remove instance metadata and propagate changes to create context. Has access to **CreateContext** (`self.create_context`) that discovered the plugin so has also access to other creators and instances. Create plugins have a lot of responsibility so it is recommended to implement common code per host.
#### *BaseCreator*
Base implementation of creator plugin. It is not recommended to use this class as base for production plugins but rather use one of **AutoCreator** and **Creator** variants.
**Abstractions**
- **`family`** (class attr) - Tells what kind of instance will be created.
```python
class WorkfileCreator(Creator):
family = "workfile"
```
- **`collect_instances`** (method) - Collect already existing instances from the workfile and add them to create context. This method is called on initialization or reset of **CreateContext**. Each creator is responsible to find its instance metadata, convert them to **CreatedInstance** object and add them to create context (`self._add_instance_to_context(instnace_obj)`).
```python
def collect_instances(self):
# Using 'pipeline.list_instances' is just example how to get existing instances from scene
# - getting existing instances is different per host implementation
for instance_data in pipeline.list_instances():
# Process only instances that were created by this creator
creator_id = instance_data.get("creator_identifier")
if creator_id == self.identifier:
# Create instance object from existing data
instance = CreatedInstance.from_existing(
instance_data, self
)
# Add instance to create context
self._add_instance_to_context(instance)
```
- **`create`** (method) - Create a new object of **CreatedInstance** store its metadata to the workfile and add the instance into the created context. Failed Creating should raise **CreatorError** if an error happens that artists can fix or give them some useful information. Triggers and implementation differs for **Creator** and **AutoCreator**.
- **`update_instances`** (method) - Update data of instances. Receives tuple with **instance** and **changes**.
```python
def update_instances(self, update_list):
# Loop over changed instances
for instance, changes in update_list:
# Example possible usage of 'changes' to use different node on change
# of node id in instance data (MADE UP)
node = None
if "node_id" in changes:
old_value, new_value = changes["node_id"]
if new_value is not None:
node = pipeline.get_node_by_id(new_value)
if node is None:
node = pipeline.get_node_by_instance_id(instance.id)
# Get node in scene that represents the instance
# Imprind data to a node
pipeline.imprint(node, instance.data_to_store())
# Most implementations will probably ignore 'changes' completely
def update_instances(self, update_list):
for instance, _ in update_list:
# Get node from scene
node = pipeline.get_node_by_instance_id(instance.id)
# Imprint data to node
pipeline.imprint(node, instance.data_to_store())
```
- **`remove_instances`** (method) - Remove instance metadata from workfile and from create context.
```python
# Possible way how to remove instance
def remove_instances(self, instances):
for instance in instances:
# Remove instance metadata from workflle
pipeline.remove_instance(instance.id)
# Remove instance from create context
self._remove_instance_from_context(instance)
# Default implementation of `AutoCreator`
def remove_instances(self, instances):
pass
```
:::note
When host implementation use universal way how to store and load instances you should implement host specific creator plugin base class with implemented **collect_instances**, **update_instances** and **remove_instances**.
:::
**Optional implementations**
- **`enabled`** (attr) - Boolean if the creator plugin is enabled and used.
- **`identifier`** (class attr) - Consistent unique string identifier of the creator plugin. Is used to identify source plugin of existing instances. There can't be 2 creator plugins with the same identifier. Default implementation returns `family` attribute.
```python
class RenderLayerCreator(Creator):
family = "render"
identifier = "render_layer"
class RenderPassCreator(Creator):
family = "render"
identifier = "render_pass"
```
- **`label`** (attr) - String label of creator plugin which will show up in UI, `identifier` is used when not set. It should be possible to use html tags.
```python
class RenderLayerCreator(Creator):
label = "Render Layer"
```
- **`get_icon`** (attr) - Icon of creator and its instances. Value can be a path to an image file, full name of qtawesome icon, `QPixmap` or `QIcon`. For complex cases or cases when `Qt` objects are returned it is recommended to override `get_icon` method and handle the logic or import `Qt` inside the method to not break headless usage of creator plugin. For list of qtawesome icons check qtawesome github repository (look for the used version in pyproject.toml). Default implementation return **icon** attribute.
- **`icon`** (method) - Attribute for default implementation of **get_icon**.
```python
class RenderLayerCreator(Creator):
# Use font awesome 5 icon
icon = "fa5.building"
```
- **`get_instance_attr_defs`** (method) - Attribute definitions of instance. Creator can define attribute values with default values for each instance. These attributes may affect how instances will be instance processed during publishing. Attribute defiitions can be used from `openpype.pipeline.lib.attribute_definitions` (NOTE: Will be moved to `openpype.lib.attribute_definitions` soon). Attribute definitions define basic types of values for different cases e.g. boolean, number, string, enumerator, etc. Default implementation returns **instance_attr_defs**.
- **`instance_attr_defs`** (attr) - Attribute for default implementation of **get_instance_attr_defs**.
```python
from openpype.pipeline import attribute_definitions
class RenderLayerCreator(Creator):
def get_instance_attr_defs(self):
# Return empty list if '_allow_farm_render' is not enabled (can be set during initialization)
if not self._allow_farm_render:
return []
# Give artist option to change if should be rendered on farm or locally
return [
attribute_definitions.BoolDef(
"render_farm",
default=False,
label="Render on Farm"
)
]
```
- **`get_subset_name`** (method) - Calculate subset name based on passed data. Data can be extended using the `get_dynamic_data` method. Default implementation is using `get_subset_name` from `openpype.lib` which is recommended.
- **`get_dynamic_data`** (method) - Can be used to extend data for subset templates which may be required in some cases.
#### *AutoCreator*
Creator that is triggered on reset of create context. Can be used for families that are expected to be created automatically without artist interaction (e.g. **workfile**). Method `create` is triggered after collecting all creators.
:::important
**AutoCreator** has implemented **remove_instances** to do nothing as removing of auto created instances would lead to creating new instance immediately or on refresh.
:::
```python
def __init__(
self, create_context, system_settings, project_settings, *args, **kwargs
):
super(MyCreator, self).__init__(
create_context, system_settings, project_settings, *args, **kwargs
)
# Get variant value from settings
variant_name = (
project_settings["my_host"][self.identifier]["variant"]
).strip()
if not variant_name:
variant_name = "Main"
self._variant_name = variant_name
# Create does not expect any arguments
def create(self):
# Look for existing instance in create context
existing_instance = None
for instance in self.create_context.instances:
if instance.creator_identifier == self.identifier:
existing_instance = instance
break
# Collect current context information
# - variant can be filled from settings
variant = self._variant_name
# Only place where we can look for current context
project_name = io.Session["AVALON_PROJECT"]
asset_name = io.Session["AVALON_ASSET"]
task_name = io.Session["AVALON_TASK"]
host_name = io.Session["AVALON_APP"]
# Create new instance if does not exist yet
if existing_instance is None:
asset_doc = io.find_one({"type": "asset", "name": asset_name})
subset_name = self.get_subset_name(
variant, task_name, asset_doc, project_name, host_name
)
data = {
"asset": asset_name,
"task": task_name,
"variant": variant
}
data.update(self.get_dynamic_data(
variant, task_name, asset_doc, project_name, host_name
))
new_instance = CreatedInstance(
self.family, subset_name, data, self
)
self._add_instance_to_context(new_instance)
# Update instance context if is not the same
elif (
existing_instance["asset"] != asset_name
or existing_instance["task"] != task_name
):
asset_doc = io.find_one({"type": "asset", "name": asset_name})
subset_name = self.get_subset_name(
variant, task_name, asset_doc, project_name, host_name
)
existing_instance["asset"] = asset_name
existing_instance["task"] = task_name
```
#### *Creator*
Implementation of creator plugin that is triggered manually by the artist in UI (or by code). Has extended options for UI purposes than **AutoCreator** and **create** method expect more arguments.
**Optional implementations**
- **`create_allow_context_change`** (class attr) - Allow to set context in UI before Creating. Some creators may not allow it or their logic would not use the context selection (e.g. bulk creators). Is set to `True` but default.
```python
class BulkRenderCreator(Creator):
create_allow_context_change = False
```
- **`get_default_variants`** (method) - Returns list of default variants that are listed in create dialog for user. Returns **default_variants** attribute by default.
- **`default_variants`** (attr) - Attribute for default implementation of **get_default_variants**.
- **`get_default_variant`** (method) - Returns default variant that is prefilled in UI (value does not have to be in default variants). By default returns **default_variant** attribute. If returns `None` then UI logic will take first item from **get_default_variants** if there is any otherwise **"Main"** is used.
- **`default_variant`** (attr) - Attribute for default implementation of **get_default_variant**.
- **`get_description`** (method) - Returns a short string description of the creator. Returns **description** attribute by default.
- **`description`** (attr) - Attribute for default implementation of **get_description**.
- **`get_detailed_description`** (method) - Returns detailed string description of creator. Can contain markdown. Returns **detailed_description** attribute by default.
- **`detailed_description`** (attr) - Attribute for default implementation of **get_detailed_description**.
- **`get_pre_create_attr_defs`** (method) - Similar to **get_instance_attr_defs** returns attribute definitions but they are filled before creation. When creation is called from UI the values are passed to **create** method. Returns **pre_create_attr_defs** attribute by default.
- **`pre_create_attr_defs`** (attr) - Attribute for default implementation of **get_pre_create_attr_defs**.
```python
from openpype.pipeline import Creator, attribute_definitions
class CreateRender(Creator):
family = "render"
label = "Render"
icon = "fa.eye"
description = "Render scene viewport"
def __init__(
self, context, system_settings, project_settings, *args, **kwargs
):
super(CreateRender, self).__init__(
context, system_settings, project_settings, *args, **kwargs
)
plugin_settings = (
project_settings["my_host"]["create"][self.__class__.__name__]
)
# Get information if studio has enabled farm publishing
self._allow_farm_render = plugin_settings["allow_farm_render"]
# Get default variants from settings
self.default_variants = plugin_settings["variants"]
def get_instance_attr_defs(self):
# Return empty list if '_allow_farm_render' is not enabled (can be set during initialization)
if not self._allow_farm_render:
return []
# Give artist option to change if should be rendered on farm or locally
return [
attribute_definitions.BoolDef(
"render_farm",
default=False,
label="Render on Farm"
)
]
def get_pre_create_attr_defs(self):
# Give user option to use selection or not
attrs = [
attribute_definitions.BoolDef(
"use_selection",
default=False,
label="Use selection"
)
]
if self._allow_farm_render:
# Set to render on farm in creator dialog
# - this value is not automatically passed to instance attributes
# creator must do that during creation
attrs.append(
attribute_definitions.BoolDef(
"render_farm",
default=False,
label="Render on Farm"
)
)
return attrs
def create(self, subset_name, instance_data, pre_create_data):
# ARGS:
# - 'subset_name' - precalculated subset name
# - 'instance_data' - context data
# - 'asset' - asset name
# - 'task' - task name
# - 'variant' - variant
# - 'family' - instnace family
# Check if should use selection or not
if pre_create_data.get("use_selection"):
items = pipeline.get_selection()
else:
items = [pipeline.create_write()]
# Validations related to selection
if len(items) > 1:
raise CreatorError("Please select only single item at time.")
elif not items:
raise CreatorError("Nothing to create. Select at least one item.")
# Create instence object
new_instance = CreatedInstance(self.family, subset_name, data, self)
# Pass value from pre create attribute to instance
# - use them only when pre create date contain the data
if "render_farm" in pre_create_data:
use_farm = pre_create_data["render_farm"]
new_instance.creator_attributes["render_farm"] = use_farm
# Store metadata to workfile
pipeline.imprint(new_instance.id, new_instance.data_to_store())
# Add instance to context
self._add_instance_to_context(new_instance)
```
## **Publish**
### Exceptions
OpenPype define few specific exceptions that should be used in publish plugins.
#### *Validation exception*
Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that errors in the plugin can be fixed by the artist himself (with or without action on plugin). Any other errors will stop publishing immediately. The exception `PublishValidationError` raised after validation order has the same effect as any other exception.
Exception `PublishValidationError` expects 4 arguments:
- **message** Which is not used in UI but for headless publishing.
- **title** Short description of error (2-5 words). Title is used for grouping of exceptions per plugin.
- **description** Detailed description of the issue where markdown and html can be used.
- **detail** Is optional to give even more detailed information for advanced users. At this moment the detail is shown directly under description but it is in plan to have detail in a collapsible widget.
Extended version is `PublishXmlValidationError` which uses xml files with stored descriptions. This helps to avoid having huge markdown texts inside code. The exception has 4 arguments:
- **plugin** The plugin object which raises the exception to find its related xml file.
- **message** Exception message for publishing without UI or different pyblish UI.
- **key** Optional argument says which error from xml is used as a validation plugin may raise error with different messages based on the current errors. Default is **"main"**.
- **formatting_data** Optional dictionary to format data in the error. This is used to fill detailed description with data from the publishing so artist can get more precise information.
**Where and how to create xml file**
Xml files for `PublishXmlValidationError` must be located in **./help** subfolder next to the plugin and the filename must match the filename of the plugin.
```
# File location related to plugin file
└ publish
├ help
│ ├ validate_scene.xml
│ └ ...
├ validate_scene.py
└ ...
```
Xml file content has **&ltroot&gt** node which may contain any amount of **&lterror&gt** nodes, but each of them must have **id** attribute with unique value. That is then used for **key**. Each error must have **&lttitle&gt** and **&ltdescription&gt** and **&ltdetail&gt**. Text content may contain python formatting keys that can be filled when an exception is raised.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<error id="main">
<title>Subset context</title>
<description>## Invalid subset context
Context of the given subset doesn't match your current scene.
### How to repair?
You can fix this with the "Repair" button on the right. This will use '{expected_asset}' asset name and overwrite '{found_asset}' asset name in scene metadata.
After that restart publishing with Reload button.
</description>
<detail>
### How could this happen?
The subset was created in different scene with different context
or the scene file was copy pasted from different context.
</detail>
</error>
</root>
```
#### *Known errors*
When there is a known error that can't be fixed by the user (e.g. can't connect to deadline service, etc.) `KnownPublishError` should be raised. The only difference is that its message is shown in UI to the artist otherwise a neutral message without context is shown.
### Plugin extension
Publish plugins can be extended by additional logic when inheriting from `OpenPypePyblishPluginMixin` which can be used as mixin (additional inheritance of class). Publish plugins that inherit from this mixin can define attributes that will be shown in **CreatedInstance**. One of the most important usages is to be able turn on/off optional plugins.
Attributes are defined by the return value of `get_attribute_defs` method. Attribute definitions are for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be re-implemented `convert_attribute_values`. Default implementation just converts the values to right types.
:::Important
Values of publish attributes from created instance are never removed automatically so implementing this method is the best way to remove legacy data or convert them to new data structure.
:::
Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`.
<details>
<summary>Example plugin</summary>
<p>
```python
import pyblish.api
from openpype.pipeline import (
OpenPypePyblishPluginMixin,
attribute_definitions,
)
# Example context plugin
class MyExtendedPlugin(
pyblish.api.ContextPlugin, OpenPypePyblishPluginMixin
):
optional = True
active = True
@classmethod
def get_attribute_defs(cls):
return [
attribute_definitions.BoolDef(
# Key under which it will be stored
"process",
# Use 'active' as default value
default=cls.active,
# Use plugin label as label for attribute
label=cls.label
)
]
def process_plugin(self, context):
# First check if plugin is optional
if not self.optional:
return True
# Attribute values are stored by class names
# - for those purposes was implemented 'get_attr_values_from_data'
# to help with accessing it
attribute_values = self.get_attr_values_from_data(context.data)
# Get 'process' key
process_value = attribute_values.get("process")
if process_value is None or process_value:
return True
return False
def process(self, context):
if not self.process_plugin(context):
return
# Do plugin logic
...
```
</p>
</details>
## **UI examples**
### Main publish window
Main window of publisher shows instances and their values, collected by creators.
**Card view**
![Publisher UI - Card view](assets/publisher_card_view.png)
**List view**
![Publisher UI - List view](assets/publisher_list_view.png)
#### *Instances views*
List of instances always contains an `Options` item which is used to show attributes of context plugins. Values from the item are saved and loaded using [host implementation](#required-functions-in-host-implementation) **get_context_data** and **update_context_data**. Instances are grouped by family and can be shown in card view (single selection) or list view (multi selection).
Instance view has at the bottom 3 buttons. Plus sign opens [create dialog](#create-dialog), bin removes selected instances and stripes swap card and list view.
#### *Context options*
It is possible to change variant or asset and task context of instances at the top part but all changes there must be confirmed. Confirmation will trigger recalculation of subset names and all new data are stored to instances.
#### *Create attributes*
Instance attributes display all created attributes of all selected instances. All attributes that have the same definition are grouped into one input and are visually indicated if values are not the same for selected instances. In most cases have **< Multiselection >** placeholder.
#### *Publish attributes*
Publish attributes work the same way as create attributes but the source of attribute definitions are pyblish plugins. Attributes are filtered based on families of selected instances and families defined in the pyblish plugin.
### Create dialog
![Publisher UI - Create dialog](assets/publisher_create_dialog.png)
Create dialog is used by artist to create new instances in a context. The context selection can be enabled/disabled by changing `create_allow_context_change` on [creator plugin](#creator). In the middle part the artist selects what will be created and what variant it is. On the right side is information about the selected creator and its pre-create attributes. There is also a question mark button which extends the window and displays more detailed information about the creator.

View file

@ -123,6 +123,10 @@ To get working connection to Google Drive there are some necessary steps:
- add new site back in OpenPype Settings, name as you want, provider needs to be 'gdrive'
- distribute credentials file via shared mounted disk location
:::note
If you are using regular personal GDrive for testing don't forget adding `/My Drive` as the prefix in root configuration. Business accounts and share drives don't need this.
:::
### SFTP
SFTP provider is used to connect to SFTP server. Currently authentication with `user:password` or `user:ssh key` is implemented.

View file

@ -136,6 +136,13 @@ module.exports = {
"dev_requirements",
"dev_build",
"dev_testing",
"dev_contribute"
"dev_contribute",
{
type: "category",
label: "Hosts integrations",
items: [
"dev_publishing"
]
}
]
};

View file

@ -5125,9 +5125,9 @@ minimatch@^3.0.4:
brace-expansion "^1.1.7"
minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@^0.5.5:
version "0.5.5"
@ -5207,9 +5207,9 @@ node-fetch@2.6.7:
whatwg-url "^5.0.0"
node-forge@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c"
integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==
version "1.3.0"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.0.tgz#37a874ea723855f37db091e6c186e5b67a01d4b2"
integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==
node-releases@^2.0.1:
version "2.0.2"