Merge branch 'feature/909-define-basic-trait-type-using-dataclasses' into feature/911-new-traits-based-integrator

This commit is contained in:
Ondrej Samohel 2025-02-13 10:11:52 +01:00
commit c8491daa54
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
17 changed files with 331 additions and 91 deletions

View file

@ -84,6 +84,10 @@ class IPluginPaths(AYONInterface):
"""Receive launcher actions paths.
Give addons ability to add launcher actions paths.
Returns:
list[str]: List of launcher action paths.
"""
return self._get_plugin_paths_by_type("actions")
@ -99,6 +103,9 @@ class IPluginPaths(AYONInterface):
Args:
host_name (str): For which host are the plugins meant.
Returns:
list[str]: List of create plugin paths.
"""
return self._get_plugin_paths_by_type("create")
@ -114,6 +121,9 @@ class IPluginPaths(AYONInterface):
Args:
host_name (str): For which host are the plugins meant.
Returns:
list[str]: List of load plugin paths.
"""
return self._get_plugin_paths_by_type("load")
@ -129,6 +139,9 @@ class IPluginPaths(AYONInterface):
Args:
host_name (str): For which host are the plugins meant.
Returns:
list[str]: List of publish plugin paths.
"""
return self._get_plugin_paths_by_type("publish")
@ -144,6 +157,9 @@ class IPluginPaths(AYONInterface):
Args:
host_name (str): For which host are the plugins meant.
Returns:
list[str]: List of inventory action plugin paths.
"""
return self._get_plugin_paths_by_type("inventory")
@ -216,8 +232,8 @@ class ITrayAddon(AYONInterface):
self,
title: str,
message: str,
icon: Optional[QtWidgets.QSystemTrayIcon]=None,
msecs: Optional[int]=None) -> None:
icon: Optional[QtWidgets.QSystemTrayIcon] = None,
msecs: Optional[int] = None) -> None:
"""Show tray message.
Args:
@ -238,7 +254,12 @@ class ITrayAddon(AYONInterface):
@staticmethod
def admin_submenu(tray_menu: QtWidgets.QMenu) -> QtWidgets.QMenu:
"""Get or create admin submenu."""
"""Get or create admin submenu.
Returns:
QtWidgets.QMenu: Admin submenu.
"""
if ITrayAddon._admin_submenu is None:
from qtpy import QtWidgets
@ -250,7 +271,16 @@ class ITrayAddon(AYONInterface):
@staticmethod
def add_action_to_admin_submenu(
label: str, tray_menu: QtWidgets.QMenu) -> QtWidgets.QAction:
"""Add action to admin submenu."""
"""Add action to admin submenu.
Args:
label (str): Label of action.
tray_menu (QtWidgets.QMenu): Tray menu to add action to.
Returns:
QtWidgets.QAction: Action added to admin submenu
"""
from qtpy import QtWidgets
menu = ITrayAddon.admin_submenu(tray_menu)
@ -297,11 +327,11 @@ class ITrayAction(ITrayAddon):
action.triggered.connect(self.on_action_trigger)
self._action_item = action
def tray_start(self) -> None:
def tray_start(self) -> None: # noqa: PLR6301
"""Start procedure in tray tool."""
return
def tray_exit(self) -> None:
def tray_exit(self) -> None: # noqa: PLR6301
"""Cleanup method which is executed on tray shutdown."""
return
@ -329,7 +359,12 @@ class ITrayService(ITrayAddon):
@staticmethod
def services_submenu(tray_menu: QtWidgets.QMenu) -> QtWidgets.QMenu:
"""Get or create services submenu."""
"""Get or create services submenu.
Returns:
QtWidgets.QMenu: Services submenu.
"""
if ITrayService._services_submenu is None:
from qtpy import QtWidgets
@ -362,21 +397,36 @@ class ITrayService(ITrayAddon):
@staticmethod
def get_icon_running() -> QtWidgets.QIcon:
"""Get running icon."""
"""Get running icon.
Returns:
QtWidgets.QIcon: Returns "running" icon.
"""
if ITrayService._icon_running is None:
ITrayService._load_service_icons()
return ITrayService._icon_running
@staticmethod
def get_icon_idle() -> QtWidgets.QIcon:
"""Get idle icon."""
"""Get idle icon.
Returns:
QtWidgets.QIcon: Returns "idle" icon.
"""
if ITrayService._icon_idle is None:
ITrayService._load_service_icons()
return ITrayService._icon_idle
@staticmethod
def get_icon_failed() -> QtWidgets.QIcon:
"""Get failed icon."""
"""Get failed icon.
Returns:
QtWidgets.QIcon: Returns "failed" icon.
"""
if ITrayService._icon_failed is None:
ITrayService._load_service_icons()
return ITrayService._icon_failed
@ -419,7 +469,7 @@ class IHostAddon(AYONInterface):
def host_name(self) -> str:
"""Name of host which addon represents."""
def get_workfile_extensions(self) -> list[str]:
def get_workfile_extensions(self) -> list[str]: # noqa: PLR6301
"""Define workfile extensions for host.
Not all hosts support workfiles thus this is optional implementation.

View file

@ -550,29 +550,38 @@ class EnumDef(AbstractAttrDef):
passed items or list of values for multiselection.
multiselection (Optional[bool]): If True, multiselection is allowed.
Output is list of selected items.
placeholder (Optional[str]): Placeholder for UI purposes, only for
multiselection enumeration.
"""
type = "enum"
type_attributes = [
"multiselection",
"placeholder",
]
def __init__(
self,
key: str,
items: "EnumItemsInputType",
default: "Union[str, List[Any]]" = None,
multiselection: Optional[bool] = False,
placeholder: Optional[str] = None,
**kwargs
):
if not items:
raise ValueError((
"Empty 'items' value. {} must have"
if multiselection is None:
multiselection = False
if not items and not multiselection:
raise ValueError(
f"Empty 'items' value. {self.__class__.__name__} must have"
" defined values on initialization."
).format(self.__class__.__name__))
)
items = self.prepare_enum_items(items)
item_values = [item["value"] for item in items]
item_values_set = set(item_values)
if multiselection is None:
multiselection = False
if multiselection:
if default is None:
@ -587,6 +596,7 @@ class EnumDef(AbstractAttrDef):
self.items: List["EnumItemDict"] = items
self._item_values: Set[Any] = item_values_set
self.multiselection: bool = multiselection
self.placeholder: Optional[str] = placeholder
def convert_value(self, value):
if not self.multiselection:
@ -612,7 +622,6 @@ class EnumDef(AbstractAttrDef):
def serialize(self):
data = super().serialize()
data["items"] = copy.deepcopy(self.items)
data["multiselection"] = self.multiselection
return data
@staticmethod

View file

@ -26,8 +26,10 @@ class Transient(TraitBase):
Args:
representation (Representation): Representation model.
Returns:
bool: True if representation is valid, False otherwise.
Raises:
TraitValidationError: If representation is marked as both
Persistent and Transient.
"""
if representation.contains_trait(Persistent):
msg = "Representation is marked as both Persistent and Transient."
@ -57,6 +59,10 @@ class Persistent(TraitBase):
Args:
representation (Representation): Representation model.
Raises:
TraitValidationError: If representation is marked
as both Persistent and Transient.
"""
if representation.contains_trait(Transient):
msg = "Representation is marked as both Persistent and Transient."

View file

@ -32,10 +32,12 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin):
for key in [
"AYON_BUNDLE_NAME",
"AYON_DEFAULT_SETTINGS_VARIANT",
"AYON_USE_STAGING",
"AYON_IN_TESTS",
# NOTE Not sure why workdir is needed?
"AYON_WORKDIR",
# DEPRECATED remove when deadline stops using it (added in 1.1.2)
"AYON_DEFAULT_SETTINGS_VARIANT",
]:
value = os.getenv(key)
if value:

View file

@ -2,7 +2,7 @@ import copy
import typing
from typing import Optional
from qtpy import QtWidgets, QtCore
from qtpy import QtWidgets, QtCore, QtGui
from ayon_core.lib.attribute_definitions import (
AbstractAttrDef,
@ -22,6 +22,8 @@ from ayon_core.tools.utils import (
FocusSpinBox,
FocusDoubleSpinBox,
MultiSelectionComboBox,
PlaceholderLineEdit,
PlaceholderPlainTextEdit,
set_style_property,
)
from ayon_core.tools.utils import NiceCheckbox
@ -502,9 +504,9 @@ class TextAttrWidget(_BaseAttrDefWidget):
self.multiline = self.attr_def.multiline
if self.multiline:
input_widget = QtWidgets.QPlainTextEdit(self)
input_widget = PlaceholderPlainTextEdit(self)
else:
input_widget = QtWidgets.QLineEdit(self)
input_widget = PlaceholderLineEdit(self)
# Override context menu event to add revert to default action
input_widget.contextMenuEvent = self._input_widget_context_event
@ -641,7 +643,9 @@ class EnumAttrWidget(_BaseAttrDefWidget):
def _ui_init(self):
if self.multiselection:
input_widget = MultiSelectionComboBox(self)
input_widget = MultiSelectionComboBox(
self, placeholder=self.attr_def.placeholder
)
else:
input_widget = CustomTextComboBox(self)
@ -655,6 +659,9 @@ class EnumAttrWidget(_BaseAttrDefWidget):
for item in self.attr_def.items:
input_widget.addItem(item["label"], item["value"])
if not self.attr_def.items:
self._add_empty_item(input_widget)
idx = input_widget.findData(self.attr_def.default)
if idx >= 0:
input_widget.setCurrentIndex(idx)
@ -671,6 +678,20 @@ class EnumAttrWidget(_BaseAttrDefWidget):
input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
input_widget.customContextMenuRequested.connect(self._on_context_menu)
def _add_empty_item(self, input_widget):
model = input_widget.model()
if not isinstance(model, QtGui.QStandardItemModel):
return
root_item = model.invisibleRootItem()
empty_item = QtGui.QStandardItem()
empty_item.setData("< No items to select >", QtCore.Qt.DisplayRole)
empty_item.setData("", QtCore.Qt.UserRole)
empty_item.setFlags(QtCore.Qt.NoItemFlags)
root_item.appendRow(empty_item)
def _on_context_menu(self, pos):
menu = QtWidgets.QMenu(self)

View file

@ -1,12 +1,18 @@
from __future__ import annotations
import time
import collections
import contextlib
import typing
from abc import ABC, abstractmethod
import ayon_api
from ayon_core.lib import NestedCacheItem
if typing.TYPE_CHECKING:
from typing import Union
HIERARCHY_MODEL_SENDER = "hierarchy.model"
@ -82,19 +88,26 @@ class TaskItem:
Args:
task_id (str): Task id.
name (str): Name of task.
name (Union[str, None]): Task label.
task_type (str): Type of task.
parent_id (str): Parent folder id.
"""
def __init__(
self, task_id, name, task_type, parent_id
self,
task_id: str,
name: str,
label: Union[str, None],
task_type: str,
parent_id: str,
):
self.task_id = task_id
self.name = name
self.label = label
self.task_type = task_type
self.parent_id = parent_id
self._label = None
self._full_label = None
@property
def id(self):
@ -107,16 +120,17 @@ class TaskItem:
return self.task_id
@property
def label(self):
def full_label(self):
"""Label of task item for UI.
Returns:
str: Label of task item.
"""
if self._label is None:
self._label = "{} ({})".format(self.name, self.task_type)
return self._label
if self._full_label is None:
label = self.label or self.name
self._full_label = f"{label} ({self.task_type})"
return self._full_label
def to_data(self):
"""Converts task item to data.
@ -128,6 +142,7 @@ class TaskItem:
return {
"task_id": self.task_id,
"name": self.name,
"label": self.label,
"parent_id": self.parent_id,
"task_type": self.task_type,
}
@ -159,6 +174,7 @@ def _get_task_items_from_tasks(tasks):
output.append(TaskItem(
task["id"],
task["name"],
task["label"],
task["type"],
folder_id
))
@ -368,7 +384,7 @@ class HierarchyModel(object):
sender (Union[str, None]): Who requested the task item.
Returns:
Union[TaskItem, None]: Task item found by name and folder id.
Optional[TaskItem]: Task item found by name and folder id.
"""
for task_item in self.get_task_items(project_name, folder_id, sender):

View file

@ -85,6 +85,8 @@ class AttributesWidget(QtWidgets.QWidget):
layout.setContentsMargins(0, 0, 0, 0)
layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING)
layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING)
layout.setColumnStretch(0, 0)
layout.setColumnStretch(1, 1)
self._layout = layout

View file

@ -5,6 +5,7 @@ from .widgets import (
ComboBox,
CustomTextComboBox,
PlaceholderLineEdit,
PlaceholderPlainTextEdit,
ElideLabel,
HintedLineEdit,
ExpandingTextEdit,
@ -89,6 +90,7 @@ __all__ = (
"ComboBox",
"CustomTextComboBox",
"PlaceholderLineEdit",
"PlaceholderPlainTextEdit",
"ElideLabel",
"HintedLineEdit",
"ExpandingTextEdit",

View file

@ -1,5 +1,7 @@
from qtpy import QtCore, QtGui, QtWidgets
from ayon_core.style import get_objected_colors
from .lib import (
checkstate_int_to_enum,
checkstate_enum_to_int,
@ -45,15 +47,16 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
top_bottom_padding = 2
left_right_padding = 3
left_offset = 4
top_bottom_margins = 2
top_bottom_margins = 1
item_spacing = 5
item_bg_color = QtGui.QColor("#31424e")
_placeholder_color = None
def __init__(
self, parent=None, placeholder="", separator=", ", **kwargs
):
super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs)
super().__init__(parent=parent, **kwargs)
self.setObjectName("MultiSelectionComboBox")
self.setFocusPolicy(QtCore.Qt.StrongFocus)
@ -61,7 +64,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True)
self._initial_mouse_pos = None
self._separator = separator
self._placeholder_text = placeholder
self._placeholder_text = placeholder or ""
delegate = ComboItemDelegate(self)
self.setItemDelegate(delegate)
@ -74,7 +77,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
return self._placeholder_text
def set_placeholder_text(self, text):
self._placeholder_text = text
self._placeholder_text = text or ""
self._update_size_hint()
def set_custom_text(self, text):
@ -206,19 +209,36 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
combotext = self._placeholder_text
else:
draw_text = False
if draw_text:
option.currentText = combotext
option.palette.setCurrentColorGroup(QtGui.QPalette.Disabled)
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option)
return
font_metricts = self.fontMetrics()
if draw_text:
color = self._get_placeholder_color()
pen = painter.pen()
pen.setColor(color)
painter.setPen(pen)
left_x = option.rect.left() + self.left_offset
font = self.font()
# This is hardcoded point size from styles
font.setPointSize(10)
painter.setFont(font)
label_rect = QtCore.QRect(option.rect)
label_rect.moveLeft(left_x)
painter.drawText(
label_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
combotext
)
return
if self._item_height is None:
self.updateGeometry()
self.update()
return
font_metrics = self.fontMetrics()
for line, items in self._lines.items():
top_y = (
option.rect.top()
@ -227,7 +247,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
)
left_x = option.rect.left() + self.left_offset
for item in items:
label_rect = font_metricts.boundingRect(item)
label_rect = font_metrics.boundingRect(item)
label_height = label_rect.height()
label_rect.moveTop(top_y)
@ -237,22 +257,25 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
label_rect.width() + self.left_right_padding
)
bg_rect = QtCore.QRectF(label_rect)
bg_rect.setWidth(
label_rect.width() + self.left_right_padding
)
left_x = bg_rect.right() + self.item_spacing
if not draw_text:
bg_rect = QtCore.QRectF(label_rect)
bg_rect.setWidth(
label_rect.width() + self.left_right_padding
)
left_x = bg_rect.right() + self.item_spacing
bg_rect.setHeight(
label_height + (2 * self.top_bottom_padding)
)
bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins)
path = QtGui.QPainterPath()
path.addRoundedRect(bg_rect, 5, 5)
painter.fillPath(path, self.item_bg_color)
label_rect.moveLeft(label_rect.x() + self.left_right_padding)
bg_rect.setHeight(label_height + (2 * self.top_bottom_padding))
bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins)
path = QtGui.QPainterPath()
path.addRoundedRect(bg_rect, 5, 5)
painter.fillPath(path, self.item_bg_color)
painter.drawText(
label_rect,
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter,
@ -287,11 +310,11 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
line = 0
self._lines = {line: []}
font_metricts = self.fontMetrics()
font_metrics = self.fontMetrics()
default_left_x = 0 + self.left_offset
left_x = int(default_left_x)
for item in items:
rect = font_metricts.boundingRect(item)
rect = font_metrics.boundingRect(item)
width = rect.width() + (2 * self.left_right_padding)
right_x = left_x + width
if right_x > total_width:
@ -382,3 +405,12 @@ class MultiSelectionComboBox(QtWidgets.QComboBox):
return event.ignore()
return super(MultiSelectionComboBox, self).keyPressEvent(event)
@classmethod
def _get_placeholder_color(cls):
if cls._placeholder_color is None:
color_obj = get_objected_colors("font")
color = color_obj.get_qcolor()
color.setAlpha(67)
cls._placeholder_color = color
return cls._placeholder_color

View file

@ -328,6 +328,9 @@ class NiceCheckbox(QtWidgets.QFrame):
if frame_rect.width() < 0 or frame_rect.height() < 0:
return
frame_rect.setLeft(frame_rect.x() + (frame_rect.width() % 2))
frame_rect.setTop(frame_rect.y() + (frame_rect.height() % 2))
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
@ -364,18 +367,23 @@ class NiceCheckbox(QtWidgets.QFrame):
margin_size_c = 0
checkbox_rect = QtCore.QRect(
frame_rect.x() + margin_size_c,
frame_rect.y() + margin_size_c,
frame_rect.width() - (margin_size_c * 2),
frame_rect.height() - (margin_size_c * 2)
frame_rect.x(),
frame_rect.y(),
frame_rect.width(),
frame_rect.height()
)
if margin_size_c:
checkbox_rect.adjust(
margin_size_c, margin_size_c,
-margin_size_c, -margin_size_c
)
if checkbox_rect.width() > checkbox_rect.height():
radius = floor(checkbox_rect.height() * 0.5)
else:
radius = floor(checkbox_rect.width() * 0.5)
painter.setPen(QtCore.Qt.transparent)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(bg_color)
painter.drawRoundedRect(checkbox_rect, radius, radius)

View file

@ -270,7 +270,7 @@ class TasksQtModel(QtGui.QStandardItemModel):
task_type_item_by_name,
task_type_icon_cache
)
item.setData(task_item.label, QtCore.Qt.DisplayRole)
item.setData(task_item.full_label, QtCore.Qt.DisplayRole)
item.setData(name, ITEM_NAME_ROLE)
item.setData(task_item.id, ITEM_ID_ROLE)
item.setData(task_item.task_type, TASK_TYPE_ROLE)

View file

@ -54,7 +54,7 @@ class ComboBox(QtWidgets.QComboBox):
"""
def __init__(self, *args, **kwargs):
super(ComboBox, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
delegate = QtWidgets.QStyledItemDelegate()
self.setItemDelegate(delegate)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
@ -63,7 +63,7 @@ class ComboBox(QtWidgets.QComboBox):
def wheelEvent(self, event):
if self.hasFocus():
return super(ComboBox, self).wheelEvent(event)
return super().wheelEvent(event)
class CustomTextComboBox(ComboBox):
@ -71,7 +71,7 @@ class CustomTextComboBox(ComboBox):
def __init__(self, *args, **kwargs):
self._custom_text = None
super(CustomTextComboBox, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
def set_custom_text(self, text=None):
if self._custom_text != text:
@ -88,23 +88,48 @@ class CustomTextComboBox(ComboBox):
painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option)
class PlaceholderLineEdit(QtWidgets.QLineEdit):
"""Set placeholder color of QLineEdit in Qt 5.12 and higher."""
def __init__(self, *args, **kwargs):
super(PlaceholderLineEdit, self).__init__(*args, **kwargs)
# Change placeholder palette color
if hasattr(QtGui.QPalette, "PlaceholderText"):
filter_palette = self.palette()
class _Cache:
_placeholder_color = None
@classmethod
def get_placeholder_color(cls):
if cls._placeholder_color is None:
color_obj = get_objected_colors("font")
color = color_obj.get_qcolor()
color.setAlpha(67)
cls._placeholder_color = color
return cls._placeholder_color
class PlaceholderLineEdit(QtWidgets.QLineEdit):
"""Set placeholder color of QLineEdit in Qt 5.12 and higher."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Change placeholder palette color
if hasattr(QtGui.QPalette, "PlaceholderText"):
filter_palette = self.palette()
filter_palette.setColor(
QtGui.QPalette.PlaceholderText,
color
_Cache.get_placeholder_color()
)
self.setPalette(filter_palette)
class PlaceholderPlainTextEdit(QtWidgets.QPlainTextEdit):
"""Set placeholder color of QPlainTextEdit in Qt 5.12 and higher."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Change placeholder palette color
if hasattr(QtGui.QPalette, "PlaceholderText"):
viewport = self.viewport()
filter_palette = viewport.palette()
filter_palette.setColor(
QtGui.QPalette.PlaceholderText,
_Cache.get_placeholder_color()
)
viewport.setPalette(filter_palette)
class ElideLabel(QtWidgets.QLabel):
"""Label which elide text.

View file

@ -484,6 +484,17 @@ DEFAULT_TOOLS_VALUES = {
"task_types": [],
"tasks": [],
"template": "{folder[name]}_{variant}"
},
{
"product_types": [
"textureSet"
],
"hosts": [
"substancedesigner"
],
"task_types": [],
"tasks": [],
"template": "T_{folder[name]}{variant}"
}
],
"filter_creator_profiles": []
@ -557,6 +568,18 @@ DEFAULT_TOOLS_VALUES = {
"task_names": [],
"template_name": "simpleUnrealTexture"
},
{
"product_types": [
"image",
"textures",
],
"hosts": [
"substancedesigner"
],
"task_types": [],
"task_names": [],
"template_name": "simpleUnrealTexture"
},
{
"product_types": [
"staticMesh",
@ -603,6 +626,18 @@ DEFAULT_TOOLS_VALUES = {
"task_types": [],
"task_names": [],
"template_name": "simpleUnrealTextureHero"
},
{
"product_types": [
"image",
"textures"
],
"hosts": [
"substancedesigner"
],
"task_types": [],
"task_names": [],
"template_name": "simpleUnrealTextureHero"
}
]
}

View file

@ -1,6 +1,7 @@
"""Tests for the content traits."""
from __future__ import annotations
import re
from pathlib import Path
import pytest
@ -59,9 +60,9 @@ def test_bundles() -> None:
sub_representation = Representation(name="test", traits=item)
assert sub_representation.contains_trait(trait=Image)
sub: MimeType = sub_representation.get_trait(trait=MimeType)
assert sub.mime_type in [
assert sub.mime_type in {
"image/jpeg", "image/tiff"
]
}
def test_file_locations_validation() -> None:
@ -94,7 +95,7 @@ def test_file_locations_validation() -> None:
)
representation.add_trait(frameranged_trait)
# it should still validate fine
# it should still validate fine
file_locations_trait.validate_trait(representation)
# create empty file locations trait
@ -165,7 +166,7 @@ def test_get_file_location_from_frame() -> None:
# test with custom regex
sequence = Sequence(
frame_padding=4,
frame_regex=r"boo_(?P<index>(?P<padding>0*)\d+)\.exr")
frame_regex=re.compile(r"boo_(?P<index>(?P<padding>0*)\d+)\.exr"))
file_locations_list = [
FileLocation(
file_path=Path(f"/path/to/boo_{frame}.exr"),

View file

@ -1,6 +1,7 @@
"""Tests for the time related traits."""
from __future__ import annotations
import re
from pathlib import Path
import pytest
@ -183,6 +184,26 @@ def test_sequence_validations() -> None:
with pytest.raises(TraitValidationError):
representation.validate()
representation = Representation(name="test_7", traits=[
FileLocations(file_paths=[
FileLocation(
file_path=Path(f"/path/to/file.{frame}.exr"),
file_size=1024,
file_hash=None,
)
for frame in range(996, 1050 + 1) # because range is zero based
]),
Sequence(
frame_padding=4,
frame_regex=re.compile(
r"img\.(?P<index>(?P<padding>0*)\d{4})\.png$")),
Handles(
frame_start_handle=5,
frame_end_handle=5,
inclusive=False
)
])
representation.validate()
def test_list_spec_to_frames() -> None:
@ -204,7 +225,7 @@ def test_list_spec_to_frames() -> None:
assert Sequence.list_spec_to_frames("1") == [1]
with pytest.raises(
ValueError,
match="Invalid frame number in the list: .*"):
match=r"Invalid frame number in the list: .*"):
Sequence.list_spec_to_frames("a")
@ -225,4 +246,3 @@ def test_sequence_get_frame_padding() -> None:
assert Sequence.get_frame_padding(
file_locations=representation.get_trait(FileLocations)) == 4

View file

@ -36,19 +36,27 @@ REPRESENTATION_DATA: dict = {
},
}
class UpgradedImage(Image):
"""Upgraded image class."""
id = "ayon.2d.Image.v2"
@classmethod
def upgrade(cls, data: dict) -> UpgradedImage: # noqa: ARG003
"""Upgrade the trait."""
"""Upgrade the trait.
Returns:
UpgradedImage: Upgraded image instance.
"""
return cls()
class InvalidTrait:
"""Invalid trait class."""
foo = "bar"
@pytest.fixture
def representation() -> Representation:
"""Return a traits data instance."""
@ -59,10 +67,11 @@ def representation() -> Representation:
Planar(**REPRESENTATION_DATA[Planar.id]),
])
def test_representation_errors(representation: Representation) -> None:
"""Test errors in representation."""
with pytest.raises(ValueError,
match="Invalid trait .* - ID is required."):
match=r"Invalid trait .* - ID is required."):
representation.add_trait(InvalidTrait())
with pytest.raises(ValueError,
@ -70,9 +79,10 @@ def test_representation_errors(representation: Representation) -> None:
representation.add_trait(Image())
with pytest.raises(ValueError,
match="Trait with ID .* not found."):
match=r"Trait with ID .* not found."):
representation.remove_trait_by_id("foo")
def test_representation_traits(representation: Representation) -> None:
"""Test setting and getting traits."""
assert representation.get_trait_by_id(
@ -143,11 +153,12 @@ def test_representation_traits(representation: Representation) -> None:
assert representation.contains_traits_by_id(
trait_ids=[FileLocation.id, Bundle.id]) is False
def test_trait_removing(representation: Representation) -> None:
"""Test removing traits."""
assert representation.contains_trait_by_id("nonexistent") is False
assert representation.contains_trait_by_id("nonexistent") is False
with pytest.raises(
ValueError, match="Trait with ID nonexistent not found."):
ValueError, match=r"Trait with ID nonexistent not found."):
representation.remove_trait_by_id("nonexistent")
assert representation.contains_trait(trait=FileLocation) is True
@ -168,6 +179,7 @@ def test_trait_removing(representation: Representation) -> None:
ValueError, match=f"Trait with ID {Image.id} not found."):
representation.remove_trait(Image)
def test_representation_dict_properties(
representation: Representation) -> None:
"""Test representation as dictionary."""
@ -224,6 +236,7 @@ def test_get_version_from_id() -> None:
assert TestMimeType(mime_type="foo/bar").get_version() is None
def test_get_versionless_id() -> None:
"""Test getting versionless trait ID."""
assert Image().get_versionless_id() == "ayon.2d.Image"
@ -271,7 +284,7 @@ def test_from_dict() -> None:
},
}
with pytest.raises(ValueError, match="Trait model with ID .* not found."):
with pytest.raises(ValueError, match=r"Trait model with ID .* not found."):
representation = Representation.from_dict(
"test", trait_data=traits_data)
@ -302,6 +315,7 @@ def test_from_dict() -> None:
"test", trait_data=traits_data)
"""
def test_representation_equality() -> None:
"""Test representation equality."""
# rep_a and rep_b are equal
@ -348,7 +362,6 @@ def test_representation_equality() -> None:
Planar(planar_configuration="RGBA"),
])
# lets assume ids are the same (because ids are randomly generated)
rep_b.representation_id = rep_d.representation_id = rep_a.representation_id
rep_c.representation_id = rep_e.representation_id = rep_a.representation_id
@ -365,6 +378,3 @@ def test_representation_equality() -> None:
assert rep_d != rep_e
# because of the trait difference
assert rep_d != rep_f

View file

@ -42,6 +42,7 @@ def test_get_file_location_for_udim() -> None:
udim=1001
) == file_locations_list[0]
def test_get_udim_from_file_location() -> None:
"""Test get_udim_from_file_location."""
file_location_1 = FileLocation(