Merge branch 'develop' into enhancement/AY-2420_Callbacks-and-groups-with-Publisher-attributes

# Conflicts:
#	client/ayon_core/tools/publisher/widgets/widgets.py
This commit is contained in:
Jakub Trllo 2024-10-08 12:51:18 +02:00
commit 165f6a6626
13 changed files with 446 additions and 55 deletions

View file

@ -28,7 +28,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"substancepainter",
"aftereffects",
"wrap",
"openrv"
"openrv",
"cinema4d"
}
launch_types = {LaunchTypes.local}

View file

@ -4,6 +4,7 @@ import collections
import uuid
import json
import copy
import warnings
from abc import ABCMeta, abstractmethod
import clique
@ -90,6 +91,30 @@ class AbstractAttrDefMeta(ABCMeta):
return obj
def _convert_reversed_attr(
main_value, depr_value, main_label, depr_label, default
):
if main_value is not None and depr_value is not None:
if main_value == depr_value:
print(
f"Got invalid '{main_label}' and '{depr_label}' arguments."
f" Using '{main_label}' value."
)
elif depr_value is not None:
warnings.warn(
(
"DEPRECATION WARNING: Using deprecated argument"
f" '{depr_label}' please use '{main_label}' instead."
),
DeprecationWarning,
stacklevel=4,
)
main_value = not depr_value
elif main_value is None:
main_value = default
return main_value
class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
"""Abstraction of attribute definition.
@ -106,12 +131,14 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
Args:
key (str): Under which key will be attribute value stored.
default (Any): Default value of an attribute.
label (str): Attribute label.
tooltip (str): Attribute tooltip.
is_label_horizontal (bool): UI specific argument. Specify if label is
next to value input or ahead.
hidden (bool): Will be item hidden (for UI purposes).
disabled (bool): Item will be visible but disabled (for UI purposes).
label (Optional[str]): Attribute label.
tooltip (Optional[str]): Attribute tooltip.
is_label_horizontal (Optional[bool]): UI specific argument. Specify
if label is next to value input or ahead.
visible (Optional[bool]): Item is shown to user (for UI purposes).
enabled (Optional[bool]): Item is enabled (for UI purposes).
hidden (Optional[bool]): DEPRECATED: Use 'visible' instead.
disabled (Optional[bool]): DEPRECATED: Use 'enabled' instead.
"""
type_attributes = []
@ -125,22 +152,28 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
label=None,
tooltip=None,
is_label_horizontal=None,
hidden=False,
disabled=False
visible=None,
enabled=None,
hidden=None,
disabled=None,
):
if is_label_horizontal is None:
is_label_horizontal = True
if hidden is None:
hidden = False
enabled = _convert_reversed_attr(
enabled, disabled, "enabled", "disabled", True
)
visible = _convert_reversed_attr(
visible, hidden, "visible", "hidden", True
)
self.key = key
self.label = label
self.tooltip = tooltip
self.default = default
self.is_label_horizontal = is_label_horizontal
self.hidden = hidden
self.disabled = disabled
self.visible = visible
self.enabled = enabled
self._id = uuid.uuid4().hex
self.__init__class__ = AbstractAttrDef
@ -149,14 +182,30 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
def id(self):
return self._id
@property
def hidden(self):
return not self.visible
@hidden.setter
def hidden(self, value):
self.visible = not value
@property
def disabled(self):
return not self.enabled
@disabled.setter
def disabled(self, value):
self.enabled = not value
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return (
self.key == other.key
and self.hidden == other.hidden
and self.default == other.default
and self.disabled == other.disabled
and self.visible == other.visible
and self.enabled == other.enabled
)
def __ne__(self, other):
@ -198,8 +247,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta):
"tooltip": self.tooltip,
"default": self.default,
"is_label_horizontal": self.is_label_horizontal,
"hidden": self.hidden,
"disabled": self.disabled
"visible": self.visible,
"enabled": self.enabled
}
for attr in self.type_attributes:
data[attr] = getattr(self, attr)
@ -279,8 +328,8 @@ class HiddenDef(AbstractAttrDef):
def __init__(self, key, default=None, **kwargs):
kwargs["default"] = default
kwargs["hidden"] = True
super(HiddenDef, self).__init__(key, **kwargs)
kwargs["visible"] = False
super().__init__(key, **kwargs)
def convert_value(self, value):
return value

View file

@ -292,13 +292,26 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end):
# Note that 24fps is slower than 25fps hence extended duration
# to preserve media range
# Compute new source range based on available rate
conformed_src_in = source_range.start_time.rescaled_to(available_range_rate)
conformed_src_duration = source_range.duration.rescaled_to(available_range_rate)
conformed_source_range = otio.opentime.TimeRange(
start_time=conformed_src_in,
duration=conformed_src_duration
)
# Compute new source range based on available rate.
# Backward-compatibility for Hiero OTIO exporter.
# NTSC compatibility might introduce floating rates, when these are
# not exactly the same (23.976 vs 23.976024627685547)
# this will cause precision issue in computation.
# Currently round to 2 decimals for comparison,
# but this should always rescale after that.
rounded_av_rate = round(available_range_rate, 2)
rounded_src_rate = round(source_range.start_time.rate, 2)
if rounded_av_rate != rounded_src_rate:
conformed_src_in = source_range.start_time.rescaled_to(available_range_rate)
conformed_src_duration = source_range.duration.rescaled_to(available_range_rate)
conformed_source_range = otio.opentime.TimeRange(
start_time=conformed_src_in,
duration=conformed_src_duration
)
else:
conformed_source_range = source_range
# modifiers
time_scalar = 1.

View file

@ -242,6 +242,26 @@ class LoaderPlugin(list):
if hasattr(self, "_fname"):
return self._fname
@classmethod
def get_representation_name_aliases(cls, representation_name: str):
"""Return representation names to which switching is allowed from
the input representation name, like an alias replacement of the input
`representation_name`.
For example, to allow an automated switch on update from representation
`ma` to `mb` or `abc`, then when `representation_name` is `ma` return:
["mb", "abc"]
The order of the names in the returned representation names is
important, because the first one existing under the new version will
be chosen.
Returns:
List[str]: Representation names switching to is allowed on update
if the input representation name is not found on the new version.
"""
return []
class ProductLoaderPlugin(LoaderPlugin):
"""Load product into host application

View file

@ -505,21 +505,6 @@ def update_container(container, version=-1):
project_name, product_entity["folderId"]
)
repre_name = current_representation["name"]
new_representation = ayon_api.get_representation_by_name(
project_name, repre_name, new_version["id"]
)
if new_representation is None:
raise ValueError(
"Representation '{}' wasn't found on requested version".format(
repre_name
)
)
path = get_representation_path(new_representation)
if not path or not os.path.exists(path):
raise ValueError("Path {} doesn't exist".format(path))
# Run update on the Loader for this container
Loader = _get_container_loader(container)
if not Loader:
@ -527,6 +512,39 @@ def update_container(container, version=-1):
"Can't update container because loader '{}' was not found."
.format(container.get("loader"))
)
repre_name = current_representation["name"]
new_representation = ayon_api.get_representation_by_name(
project_name, repre_name, new_version["id"]
)
if new_representation is None:
# The representation name is not found in the new version.
# Allow updating to a 'matching' representation if the loader
# has defined compatible update conversions
repre_name_aliases = Loader.get_representation_name_aliases(repre_name)
if repre_name_aliases:
representations = ayon_api.get_representations(
project_name,
representation_names=repre_name_aliases,
version_ids=[new_version["id"]])
representations_by_name = {
repre["name"]: repre for repre in representations
}
for name in repre_name_aliases:
if name in representations_by_name:
new_representation = representations_by_name[name]
break
if new_representation is None:
raise ValueError(
"Representation '{}' wasn't found on requested version".format(
repre_name
)
)
path = get_representation_path(new_representation)
if not path or not os.path.exists(path):
raise ValueError("Path {} doesn't exist".format(path))
project_entity = ayon_api.get_project(project_name)
context = {
"project": project_entity,

View file

@ -28,10 +28,10 @@ from .files_widget import FilesWidget
def create_widget_for_attr_def(attr_def, parent=None):
widget = _create_widget_for_attr_def(attr_def, parent)
if attr_def.hidden:
if not attr_def.visible:
widget.setVisible(False)
if attr_def.disabled:
if not attr_def.enabled:
widget.setEnabled(False)
return widget
@ -135,7 +135,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget):
widget = create_widget_for_attr_def(attr_def, self)
self._widgets.append(widget)
if attr_def.hidden:
if not attr_def.visible:
continue
expand_cols = 2

View file

@ -32,17 +32,20 @@ PLUGIN_ORDER_OFFSET = 0.5
class MessageHandler(logging.Handler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.records = []
self._records = []
def clear_records(self):
self.records = []
self._records = []
def emit(self, record):
try:
record.msg = record.getMessage()
except Exception:
record.msg = str(record.msg)
self.records.append(record)
self._records.append(record)
def get_records(self):
return self._records
class PublishErrorInfo:
@ -1328,7 +1331,18 @@ class PublishModel:
plugin, self._publish_context, instance
)
if log_handler is not None:
result["records"] = log_handler.records
records = log_handler.get_records()
exception = result.get("error")
if exception is not None and records:
last_record = records[-1]
if (
last_record.name == "pyblish.plugin"
and last_record.levelno == logging.ERROR
):
# Remove last record made by pyblish
# - `log.exception(formatted_traceback)`
records.pop(-1)
result["records"] = records
exception = result.get("error")
if exception:

View file

@ -111,7 +111,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget):
self._attr_def_id_to_instances[attr_def.id] = instance_ids
self._attr_def_id_to_attr_def[attr_def.id] = attr_def
if attr_def.hidden:
if not attr_def.visible:
continue
expand_cols = 2
@ -282,15 +282,15 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget):
widget = create_widget_for_attr_def(
attr_def, content_widget
)
hidden_widget = attr_def.hidden
visible_widget = attr_def.visible
# Hide unknown values of publish plugins
# - The keys in most of the cases does not represent what
# would label represent
if isinstance(attr_def, UnknownDef):
widget.setVisible(False)
hidden_widget = True
visible_widget = False
if not hidden_widget:
if visible_widget:
expand_cols = 2
if attr_def.is_value_def and attr_def.is_label_horizontal:
expand_cols = 1

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring AYON addon 'core' version."""
__version__ = "1.0.0+dev"
__version__ = "1.0.1+dev"

View file

@ -1,6 +1,6 @@
name = "core"
title = "Core"
version = "1.0.0+dev"
version = "1.0.1+dev"
client_dir = "ayon_core"

View file

@ -5,7 +5,7 @@
[tool.poetry]
name = "ayon-core"
version = "1.0.0+dev"
version = "1.0.1+dev"
description = ""
authors = ["Ynput Team <team@ynput.io>"]
readme = "README.md"

View file

@ -0,0 +1,255 @@
{
"OTIO_SCHEMA": "Clip.2",
"metadata": {
"active": true,
"applieswhole": 1,
"asset": "sh020",
"audio": true,
"families": [
"clip"
],
"family": "plate",
"handleEnd": 8,
"handleStart": 0,
"heroTrack": true,
"hierarchy": "shots/sq001",
"hierarchyData": {
"episode": "ep01",
"folder": "shots",
"sequence": "sq001",
"shot": "sh020",
"track": "reference"
},
"hiero_source_type": "TrackItem",
"id": "pyblish.avalon.instance",
"label": "openpypeData",
"note": "OpenPype data container",
"parents": [
{
"entity_name": "shots",
"entity_type": "folder"
},
{
"entity_name": "sq001",
"entity_type": "sequence"
}
],
"publish": true,
"reviewTrack": null,
"sourceResolution": false,
"subset": "plateP01",
"variant": "Main",
"workfileFrameStart": 1001
},
"name": "sh020",
"source_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 23.976024627685547,
"value": 51.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 23.976024627685547,
"value": 0.0
}
},
"effects": [],
"markers": [
{
"OTIO_SCHEMA": "Marker.2",
"metadata": {
"active": true,
"applieswhole": 1,
"asset": "sh020",
"audio": true,
"families": [
"clip"
],
"family": "plate",
"handleEnd": 8,
"handleStart": 0,
"heroTrack": true,
"hierarchy": "shots/sq001",
"hierarchyData": {
"episode": "ep01",
"folder": "shots",
"sequence": "sq001",
"shot": "sh020",
"track": "reference"
},
"hiero_source_type": "TrackItem",
"id": "pyblish.avalon.instance",
"label": "openpypeData",
"note": "OpenPype data container",
"parents": [
{
"entity_name": "shots",
"entity_type": "folder"
},
{
"entity_name": "sq001",
"entity_type": "sequence"
}
],
"publish": true,
"reviewTrack": null,
"sourceResolution": false,
"subset": "plateP01",
"variant": "Main",
"workfileFrameStart": 1001
},
"name": "openpypeData",
"color": "RED",
"marked_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 23.976024627685547,
"value": 0.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 23.976024627685547,
"value": 0.0
}
}
},
{
"OTIO_SCHEMA": "Marker.2",
"metadata": {
"applieswhole": 1,
"family": "task",
"hiero_source_type": "TrackItem",
"label": "comp",
"note": "Compositing",
"type": "Compositing"
},
"name": "comp",
"color": "RED",
"marked_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 23.976024627685547,
"value": 0.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 23.976024627685547,
"value": 0.0
}
}
}
],
"enabled": true,
"media_references": {
"DEFAULT_MEDIA": {
"OTIO_SCHEMA": "ImageSequenceReference.1",
"metadata": {
"clip.properties.blendfunc": "0",
"clip.properties.colourspacename": "default",
"clip.properties.domainroot": "",
"clip.properties.enabled": "1",
"clip.properties.expanded": "1",
"clip.properties.opacity": "1",
"clip.properties.valuesource": "",
"foundry.source.audio": "",
"foundry.source.bitmapsize": "0",
"foundry.source.bitsperchannel": "0",
"foundry.source.channelformat": "integer",
"foundry.source.colourtransform": "ACES - ACES2065-1",
"foundry.source.duration": "59",
"foundry.source.filename": "MER_sq001_sh020_P01.%04d.exr 997-1055",
"foundry.source.filesize": "",
"foundry.source.fragments": "59",
"foundry.source.framerate": "23.98",
"foundry.source.fullpath": "",
"foundry.source.height": "1080",
"foundry.source.layers": "colour",
"foundry.source.path": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.%04d.exr 997-1055",
"foundry.source.pixelAspect": "1",
"foundry.source.pixelAspectRatio": "",
"foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11",
"foundry.source.reelID": "",
"foundry.source.resolution": "",
"foundry.source.samplerate": "Invalid",
"foundry.source.shortfilename": "MER_sq001_sh020_P01.%04d.exr 997-1055",
"foundry.source.shot": "",
"foundry.source.shotDate": "",
"foundry.source.startTC": "",
"foundry.source.starttime": "997",
"foundry.source.timecode": "172800",
"foundry.source.umid": "1bf7437a-b446-440c-07c5-7cae7acf4f5e",
"foundry.source.umidOriginator": "foundry.source.umid",
"foundry.source.width": "1920",
"foundry.timeline.autodiskcachemode": "Manual",
"foundry.timeline.colorSpace": "ACES - ACES2065-1",
"foundry.timeline.duration": "59",
"foundry.timeline.framerate": "23.98",
"foundry.timeline.outputformat": "",
"foundry.timeline.poster": "0",
"foundry.timeline.posterLayer": "colour",
"foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAMAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=",
"foundry.timeline.samplerate": "Invalid",
"isSequence": true,
"media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}",
"media.exr.compression": "8",
"media.exr.compressionName": "DWAA",
"media.exr.dataWindow": "0,0,1919,1079",
"media.exr.displayWindow": "0,0,1919,1079",
"media.exr.dwaCompressionLevel": "90",
"media.exr.lineOrder": "0",
"media.exr.pixelAspectRatio": "1",
"media.exr.screenWindowCenter": "0,0",
"media.exr.screenWindowWidth": "1",
"media.exr.type": "scanlineimage",
"media.exr.version": "1",
"media.input.bitsperchannel": "16-bit half float",
"media.input.ctime": "2022-04-21 11:56:03",
"media.input.filename": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01/MER_sq001_sh020_P01.0997.exr",
"media.input.filereader": "exr",
"media.input.filesize": "1235182",
"media.input.frame": "1",
"media.input.frame_rate": "23.976",
"media.input.height": "1080",
"media.input.mtime": "2022-03-06 10:14:41",
"media.input.timecode": "02:00:00:00",
"media.input.width": "1920",
"media.nuke.full_layer_names": "0",
"media.nuke.node_hash": "ffffffffffffffff",
"media.nuke.version": "12.2v3",
"openpype.source.colourtransform": "ACES - ACES2065-1",
"openpype.source.height": 1080,
"openpype.source.pixelAspect": 1.0,
"openpype.source.width": 1920,
"padding": 4
},
"name": "",
"available_range": {
"OTIO_SCHEMA": "TimeRange.1",
"duration": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 23.976,
"value": 59.0
},
"start_time": {
"OTIO_SCHEMA": "RationalTime.1",
"rate": 23.976,
"value": 997.0
}
},
"available_image_bounds": null,
"target_url_base": "C:/projects/AY01_VFX_demo/resources/plates/MER_sq001_sh020_P01\\",
"name_prefix": "MER_sq001_sh020_P01.",
"name_suffix": ".exr",
"start_frame": 997,
"frame_step": 1,
"rate": 23.976,
"frame_zero_padding": 4,
"missing_frame_policy": "error"
}
},
"active_media_reference_key": "DEFAULT_MEDIA"
}

View file

@ -166,3 +166,24 @@ def test_img_sequence_relative_source_range():
"legacy_img_sequence.json",
expected_data
)
def test_img_sequence_conform_to_23_976fps():
"""
Img sequence clip
available files = 997-1047 23.976fps
source_range = 997-1055 23.976024627685547fps
"""
expected_data = {
'mediaIn': 997,
'mediaOut': 1047,
'handleStart': 0,
'handleEnd': 8,
'speed': 1.0
}
_check_expected_retimed_values(
"img_seq_23.976_metadata.json",
expected_data,
handle_start=0,
handle_end=8,
)