mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge remote-tracking branch 'origin/feature/909-define-basic-trait-type-using-dataclasses' into feature/911-new-traits-based-integrator
This commit is contained in:
commit
ce62f7d152
7 changed files with 699 additions and 221 deletions
|
|
@ -1,6 +1,8 @@
|
|||
"""Content traits for the pipeline."""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
|
||||
# TC003 is there because Path in TYPECHECKING will fail in tests
|
||||
from pathlib import Path # noqa: TC003
|
||||
from typing import ClassVar, Optional
|
||||
|
|
@ -8,7 +10,7 @@ from typing import ClassVar, Optional
|
|||
from pydantic import Field
|
||||
|
||||
from .representation import Representation
|
||||
from .time import FrameRanged, Sequence
|
||||
from .time import FrameRanged, Handles, Sequence
|
||||
from .trait import (
|
||||
MissingTraitError,
|
||||
TraitBase,
|
||||
|
|
@ -125,20 +127,55 @@ class FileLocations(TraitBase):
|
|||
# If there are no file paths, we can't validate
|
||||
msg = "No file locations defined (empty list)"
|
||||
raise TraitValidationError(self.name, msg)
|
||||
if representation.contains_trait(FrameRanged):
|
||||
self._validate_frame_range(representation)
|
||||
|
||||
def _validate_frame_range(self, representation: Representation) -> None:
|
||||
"""Validate the frame range against the file paths.
|
||||
|
||||
If the representation contains a FrameRanged trait, this method will
|
||||
validate the frame range against the file paths. If the frame range
|
||||
does not match the file paths, the trait is invalid. It takes into
|
||||
account the Handles and Sequence traits.
|
||||
|
||||
Args:
|
||||
representation (Representation): Representation to validate.
|
||||
|
||||
Raises:
|
||||
TraitValidationError: If the trait is invalid within the
|
||||
representation.
|
||||
|
||||
"""
|
||||
tmp_frame_ranged: FrameRanged = get_sequence_from_files(
|
||||
[f.file_path for f in self.file_paths])
|
||||
|
||||
frames_from_spec = None
|
||||
try:
|
||||
with contextlib.suppress(MissingTraitError):
|
||||
sequence: Sequence = representation.get_trait(Sequence)
|
||||
if sequence.frame_spec:
|
||||
frames_from_spec: list[int] = sequence.get_frame_list(
|
||||
self, sequence.frame_regex)
|
||||
|
||||
except MissingTraitError:
|
||||
# If there is no sequence trait, we can't validate it
|
||||
pass
|
||||
frame_start_with_handles, frame_end_with_handles = \
|
||||
self._get_frame_info_with_handles(representation, frames_from_spec)
|
||||
|
||||
if frame_start_with_handles \
|
||||
and tmp_frame_ranged.frame_start != frame_start_with_handles:
|
||||
# If the detected frame range does not match the combined
|
||||
# FrameRanged and Handles trait, the
|
||||
# trait is invalid.
|
||||
msg = (
|
||||
f"Frame range defined by {self.name} "
|
||||
f"({tmp_frame_ranged.frame_start}-"
|
||||
f"{tmp_frame_ranged.frame_end}) "
|
||||
"in files does not match "
|
||||
"frame range "
|
||||
f"({frame_start_with_handles}-"
|
||||
f"{frame_end_with_handles}) defined in FrameRanged trait."
|
||||
)
|
||||
|
||||
raise TraitValidationError(self.name, msg)
|
||||
|
||||
if frames_from_spec:
|
||||
if len(frames_from_spec) != len(self.file_paths) :
|
||||
# If the number of file paths does not match the frame range,
|
||||
|
|
@ -155,40 +192,94 @@ class FileLocations(TraitBase):
|
|||
# the rest is validated by Sequence validators.
|
||||
return
|
||||
|
||||
length_with_handles: int = (
|
||||
frame_end_with_handles - frame_start_with_handles + 1
|
||||
)
|
||||
|
||||
if len(self.file_paths) - 1 != \
|
||||
tmp_frame_ranged.frame_end - tmp_frame_ranged.frame_start:
|
||||
if len(self.file_paths) != length_with_handles:
|
||||
# If the number of file paths does not match the frame range,
|
||||
# the trait is invalid
|
||||
msg = (
|
||||
f"Number of file locations ({len(self.file_paths) - 1}) "
|
||||
f"Number of file locations ({len(self.file_paths)}) "
|
||||
"does not match frame range "
|
||||
f"({tmp_frame_ranged.frame_end - tmp_frame_ranged.frame_start})" # noqa: E501
|
||||
f"({length_with_handles})"
|
||||
)
|
||||
raise TraitValidationError(self.name, msg)
|
||||
|
||||
try:
|
||||
frame_ranged: FrameRanged = representation.get_trait(FrameRanged)
|
||||
frame_ranged: FrameRanged = representation.get_trait(FrameRanged)
|
||||
|
||||
if frame_ranged.frame_start != tmp_frame_ranged.frame_start or \
|
||||
frame_ranged.frame_end != tmp_frame_ranged.frame_end:
|
||||
# If the frame range does not match the sequence trait, the
|
||||
# trait is invalid. Note that we don't check the frame rate
|
||||
# because it is not stored in the file paths and is not
|
||||
# determined by `get_sequence_from_files`.
|
||||
msg = (
|
||||
"Frame range "
|
||||
f"({frame_ranged.frame_start}-{frame_ranged.frame_end}) "
|
||||
"in sequence trait does not match "
|
||||
"frame range "
|
||||
f"({tmp_frame_ranged.frame_start}-{tmp_frame_ranged.frame_end}) " # noqa: E501
|
||||
"defined in files."
|
||||
)
|
||||
raise TraitValidationError(self.name, msg)
|
||||
if frame_start_with_handles != tmp_frame_ranged.frame_start or \
|
||||
frame_end_with_handles != tmp_frame_ranged.frame_end:
|
||||
# If the frame range does not match the FrameRanged trait, the
|
||||
# trait is invalid. Note that we don't check the frame rate
|
||||
# because it is not stored in the file paths and is not
|
||||
# determined by `get_sequence_from_files`.
|
||||
msg = (
|
||||
"Frame range "
|
||||
f"({frame_ranged.frame_start}-{frame_ranged.frame_end}) "
|
||||
"in sequence trait does not match "
|
||||
"frame range "
|
||||
f"({tmp_frame_ranged.frame_start}-"
|
||||
f"{tmp_frame_ranged.frame_end}) "
|
||||
)
|
||||
raise TraitValidationError(self.name, msg)
|
||||
|
||||
except MissingTraitError:
|
||||
# If there is no frame_ranged trait, we can't validate it
|
||||
pass
|
||||
def _get_frame_info_with_handles(
|
||||
self,
|
||||
representation: Representation,
|
||||
frames_from_spec: list[int]) -> tuple[int, int]:
|
||||
"""Get the frame range with handles from the representation.
|
||||
|
||||
This will return frame start and frame end with handles calculated
|
||||
in if there actually is the Handles trait in the representation.
|
||||
|
||||
Args:
|
||||
representation (Representation): Representation to get the frame
|
||||
range from.
|
||||
frames_from_spec (list[int]): List of frames from the frame spec.
|
||||
This list is modified in place to take into
|
||||
account the handles.
|
||||
|
||||
Mutates:
|
||||
frames_from_spec: List of frames from the frame spec.
|
||||
|
||||
Returns:
|
||||
tuple[int, int]: Start and end frame with handles.
|
||||
|
||||
"""
|
||||
frame_start = frame_end = 0
|
||||
frame_start_handle = frame_end_handle = 0
|
||||
# If there is no sequence trait, we can't validate it
|
||||
if frames_from_spec and representation.contains_trait(FrameRanged):
|
||||
# if there is no FrameRanged trait (but really there should be)
|
||||
# we can use the frame range from the frame spec
|
||||
frame_start = min(frames_from_spec)
|
||||
frame_end = max(frames_from_spec)
|
||||
|
||||
# Handle the frame range
|
||||
with contextlib.suppress(MissingTraitError):
|
||||
frame_start = representation.get_trait(FrameRanged).frame_start
|
||||
frame_end = representation.get_trait(FrameRanged).frame_end
|
||||
|
||||
# Handle the handles :P
|
||||
with contextlib.suppress(MissingTraitError):
|
||||
handles: Handles = representation.get_trait(Handles)
|
||||
if not handles.inclusive:
|
||||
# if handless are exclusive, we need to adjust the frame range
|
||||
frame_start_handle = handles.frame_start_handle
|
||||
frame_end_handle = handles.frame_end_handle
|
||||
if frames_from_spec:
|
||||
frames_from_spec.extend(
|
||||
range(frame_start - frame_start_handle, frame_start)
|
||||
)
|
||||
frames_from_spec.extend(
|
||||
range(frame_end + 1, frame_end_handle + frame_end + 1)
|
||||
)
|
||||
|
||||
frame_start_with_handles = frame_start - frame_start_handle
|
||||
frame_end_with_handles = frame_end + frame_end_handle
|
||||
|
||||
return frame_start_with_handles, frame_end_with_handles
|
||||
|
||||
|
||||
class RootlessLocation(TraitBase):
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Defines the base trait model and representation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
|
|
@ -49,7 +50,7 @@ class Representation:
|
|||
representation_id (str): Representation ID.
|
||||
|
||||
"""
|
||||
_data: dict
|
||||
_data: dict[str, T]
|
||||
_module_blacklist: ClassVar[list[str]] = [
|
||||
"_", "builtins", "pydantic",
|
||||
]
|
||||
|
|
@ -60,6 +61,70 @@ class Representation:
|
|||
"""Return hash of the representation ID."""
|
||||
return hash(self.representation_id)
|
||||
|
||||
def __getitem__(self, key: str) -> T:
|
||||
"""Get the trait by ID.
|
||||
|
||||
Args:
|
||||
key (str): Trait ID.
|
||||
|
||||
Returns:
|
||||
TraitBase: Trait instance.
|
||||
|
||||
Raises:
|
||||
MissingTraitError: If the trait is not found.
|
||||
|
||||
"""
|
||||
return self.get_trait_by_id(key)
|
||||
|
||||
def __setitem__(self, key: str, value: T) -> None:
|
||||
"""Set the trait by ID.
|
||||
|
||||
Args:
|
||||
key (str): Trait ID.
|
||||
value (TraitBase): Trait instance.
|
||||
|
||||
"""
|
||||
with contextlib.suppress(KeyError):
|
||||
self._data.pop(key)
|
||||
|
||||
self.add_trait(value)
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
"""Remove the trait by ID.
|
||||
|
||||
Args:
|
||||
key (str): Trait ID.
|
||||
|
||||
Raises:
|
||||
ValueError: If the trait is not found.
|
||||
|
||||
"""
|
||||
self.remove_trait_by_id(key)
|
||||
|
||||
def __contains__(self, key: str) -> bool:
|
||||
"""Check if the trait exists by ID.
|
||||
|
||||
Args:
|
||||
key (str): Trait ID.
|
||||
|
||||
Returns:
|
||||
bool: True if the trait exists, False otherwise.
|
||||
|
||||
"""
|
||||
return self.contains_trait_by_id(key)
|
||||
|
||||
def __iter__(self):
|
||||
"""Return the trait ID iterator."""
|
||||
return iter(self._data)
|
||||
|
||||
def __str__(self):
|
||||
"""Return the representation name."""
|
||||
return self.name
|
||||
|
||||
def items(self) -> dict[str, T]:
|
||||
"""Return the traits as items."""
|
||||
return self._data.items()
|
||||
|
||||
def add_trait(self, trait: TraitBase, *, exists_ok: bool=False) -> None:
|
||||
"""Add a trait to the Representation.
|
||||
|
||||
|
|
@ -365,7 +430,7 @@ class Representation:
|
|||
match = re.search(version_regex, trait_id)
|
||||
return int(match[1]) if match else None
|
||||
|
||||
def __eq__(self, other: Representation) -> bool: # noqa: PLR0911
|
||||
def __eq__(self, other: object) -> bool: # noqa: PLR0911
|
||||
"""Check if the representation is equal to another.
|
||||
|
||||
Args:
|
||||
|
|
@ -375,10 +440,10 @@ class Representation:
|
|||
bool: True if the representations are equal, False otherwise.
|
||||
|
||||
"""
|
||||
if self.representation_id != other.representation_id:
|
||||
if not isinstance(other, Representation):
|
||||
return False
|
||||
|
||||
if not isinstance(other, Representation):
|
||||
if self.representation_id != other.representation_id:
|
||||
return False
|
||||
|
||||
if self.name != other.name:
|
||||
|
|
@ -393,9 +458,6 @@ class Representation:
|
|||
return False
|
||||
if trait != other._data[trait_id]:
|
||||
return False
|
||||
for key, value in trait.model_dump().items():
|
||||
if value != other._data[trait_id].model_dump().get(key):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -618,4 +680,5 @@ class Representation:
|
|||
bool: True if the representation is valid, False otherwise.
|
||||
|
||||
"""
|
||||
return all(trait.validate(self) for trait in self._data.values())
|
||||
for trait in self._data.values():
|
||||
trait.validate(self)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""Temporal (time related) traits."""
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from enum import Enum, auto
|
||||
from typing import TYPE_CHECKING, ClassVar, Optional
|
||||
|
||||
|
|
@ -137,20 +138,62 @@ class Sequence(TraitBase):
|
|||
|
||||
# if there is FileLocations trait, run validation
|
||||
# on it as well
|
||||
try:
|
||||
from .content import FileLocations
|
||||
file_locs: FileLocations = representation.get_trait(
|
||||
FileLocations)
|
||||
file_locs.validate(representation)
|
||||
# validate if file locations on representation
|
||||
# matches the frame list (if any)
|
||||
self.validate_frame_list(file_locs)
|
||||
self.validate_frame_padding(file_locs)
|
||||
except MissingTraitError:
|
||||
pass
|
||||
|
||||
with contextlib.suppress(MissingTraitError):
|
||||
self._validate_file_locations(representation)
|
||||
|
||||
def _validate_file_locations(self, representation: Representation) -> None:
|
||||
"""Validate file locations trait.
|
||||
|
||||
If along with the Sequence trait, there is a FileLocations trait,
|
||||
then we need to validate if the file locations match the frame
|
||||
list specification.
|
||||
|
||||
Args:
|
||||
representation (Representation): Representation instance.
|
||||
|
||||
Raises:
|
||||
TraitValidationError: If file locations do not match the
|
||||
frame list specification
|
||||
|
||||
"""
|
||||
from .content import FileLocations
|
||||
file_locs: FileLocations = representation.get_trait(
|
||||
FileLocations)
|
||||
# validate if file locations on representation
|
||||
# matches the frame list (if any)
|
||||
# we need to extend the expected frames with Handles
|
||||
frame_start = None
|
||||
frame_end = None
|
||||
handles_frame_start = None
|
||||
handles_frame_end = None
|
||||
with contextlib.suppress(MissingTraitError):
|
||||
handles: Handles = representation.get_trait(Handles)
|
||||
# if handles are inclusive, they should be already
|
||||
# accounted in the FrameRaged frame spec
|
||||
if not handles.inclusive:
|
||||
handles_frame_start = handles.frame_start_handle
|
||||
handles_frame_end = handles.frame_end_handle
|
||||
with contextlib.suppress(MissingTraitError):
|
||||
frame_ranged: FrameRanged = representation.get_trait(
|
||||
FrameRanged)
|
||||
frame_start = frame_ranged.frame_start
|
||||
frame_end = frame_ranged.frame_end
|
||||
self.validate_frame_list(
|
||||
file_locs,
|
||||
frame_start,
|
||||
frame_end,
|
||||
handles_frame_start,
|
||||
handles_frame_end)
|
||||
self.validate_frame_padding(file_locs)
|
||||
|
||||
def validate_frame_list(
|
||||
self, file_locations: FileLocations) -> None:
|
||||
self,
|
||||
file_locations: FileLocations,
|
||||
frame_start: Optional[int] = None,
|
||||
frame_end: Optional[int] = None,
|
||||
handles_frame_start: Optional[int] = None,
|
||||
handles_frame_end: Optional[int] = None) -> None:
|
||||
"""Validate frame list.
|
||||
|
||||
This will take FileLocations trait and validate if the
|
||||
|
|
@ -164,6 +207,10 @@ class Sequence(TraitBase):
|
|||
|
||||
Args:
|
||||
file_locations (FileLocations): File locations trait.
|
||||
frame_start (Optional[int]): Frame start.
|
||||
frame_end (Optional[int]): Frame end.
|
||||
handles_frame_start (Optional[int]): Frame start handle.
|
||||
handles_frame_end (Optional[int]): Frame end handle.
|
||||
|
||||
Raises:
|
||||
TraitValidationError: If frame list does not match
|
||||
|
|
@ -177,6 +224,32 @@ class Sequence(TraitBase):
|
|||
file_locations, self.frame_regex)
|
||||
|
||||
expected_frames = self.list_spec_to_frames(self.frame_spec)
|
||||
if frame_start is None or frame_end is None:
|
||||
if min(expected_frames) != frame_start:
|
||||
msg = (
|
||||
"Frame start does not match the expected frame start. "
|
||||
f"Expected: {frame_start}, Found: {min(expected_frames)}"
|
||||
)
|
||||
raise TraitValidationError(self.name, msg)
|
||||
|
||||
if max(expected_frames) != frame_end:
|
||||
msg = (
|
||||
"Frame end does not match the expected frame end. "
|
||||
f"Expected: {frame_end}, Found: {max(expected_frames)}"
|
||||
)
|
||||
raise TraitValidationError(self.name, msg)
|
||||
|
||||
# we need to extend the expected frames with Handles
|
||||
if handles_frame_start is not None:
|
||||
expected_frames.extend(
|
||||
range(
|
||||
min(frames) - handles_frame_start, min(frames) + 1))
|
||||
|
||||
if handles_frame_end is not None:
|
||||
expected_frames.extend(
|
||||
range(
|
||||
max(frames), max(frames) + handles_frame_end + 1))
|
||||
|
||||
if set(frames) != set(expected_frames):
|
||||
msg = (
|
||||
"Frame list does not match the expected frames. "
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from pydantic import (
|
|||
AliasGenerator,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -32,6 +33,10 @@ class TraitBase(ABC, BaseModel):
|
|||
)
|
||||
)
|
||||
|
||||
persitent: bool = Field(
|
||||
default=True, title="Persitent",
|
||||
description="Whether the trait is persistent (integrated) or not.")
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def id(self) -> str:
|
||||
|
|
|
|||
119
tests/client/ayon_core/pipeline/traits/test_content_traits.py
Normal file
119
tests/client/ayon_core/pipeline/traits/test_content_traits.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""Tests for the content traits."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from ayon_core.pipeline.traits import (
|
||||
Bundle,
|
||||
FileLocation,
|
||||
FileLocations,
|
||||
FrameRanged,
|
||||
Image,
|
||||
MimeType,
|
||||
PixelBased,
|
||||
Planar,
|
||||
Representation,
|
||||
)
|
||||
from ayon_core.pipeline.traits.trait import TraitValidationError
|
||||
|
||||
|
||||
def test_bundles() -> None:
|
||||
"""Test bundle trait."""
|
||||
diffuse_texture = [
|
||||
Image(),
|
||||
PixelBased(
|
||||
display_window_width=1920,
|
||||
display_window_height=1080,
|
||||
pixel_aspect_ratio=1.0),
|
||||
Planar(planar_configuration="RGB"),
|
||||
FileLocation(
|
||||
file_path=Path("/path/to/diffuse.jpg"),
|
||||
file_size=1024,
|
||||
file_hash=None),
|
||||
MimeType(mime_type="image/jpeg"),
|
||||
]
|
||||
bump_texture = [
|
||||
Image(),
|
||||
PixelBased(
|
||||
display_window_width=1920,
|
||||
display_window_height=1080,
|
||||
pixel_aspect_ratio=1.0),
|
||||
Planar(planar_configuration="RGB"),
|
||||
FileLocation(
|
||||
file_path=Path("/path/to/bump.tif"),
|
||||
file_size=1024,
|
||||
file_hash=None),
|
||||
MimeType(mime_type="image/tiff"),
|
||||
]
|
||||
bundle = Bundle(items=[diffuse_texture, bump_texture])
|
||||
representation = Representation(name="test_bundle", traits=[bundle])
|
||||
|
||||
if representation.contains_trait(trait=Bundle):
|
||||
assert representation.get_trait(trait=Bundle).items == [
|
||||
diffuse_texture, bump_texture
|
||||
]
|
||||
|
||||
for item in representation.get_trait(trait=Bundle).items:
|
||||
sub_representation = Representation(name="test", traits=item)
|
||||
assert sub_representation.contains_trait(trait=Image)
|
||||
assert sub_representation.get_trait(trait=MimeType).mime_type in [
|
||||
"image/jpeg", "image/tiff"
|
||||
]
|
||||
|
||||
|
||||
def test_file_locations_validation() -> None:
|
||||
"""Test FileLocations trait validation."""
|
||||
file_locations_list = [
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1051)
|
||||
]
|
||||
|
||||
representation = Representation(name="test", traits=[
|
||||
FileLocations(file_paths=file_locations_list)
|
||||
])
|
||||
|
||||
file_locations_trait: FileLocations = FileLocations(
|
||||
file_paths=file_locations_list)
|
||||
|
||||
# this should be valid trait
|
||||
file_locations_trait.validate(representation)
|
||||
|
||||
# add valid FrameRanged trait
|
||||
sequence_trait = FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1050,
|
||||
frames_per_second="25"
|
||||
)
|
||||
representation.add_trait(sequence_trait)
|
||||
|
||||
# it should still validate fine
|
||||
file_locations_trait.validate(representation)
|
||||
|
||||
# create empty file locations trait
|
||||
empty_file_locations_trait = FileLocations(file_paths=[])
|
||||
representation = Representation(name="test", traits=[
|
||||
empty_file_locations_trait
|
||||
])
|
||||
with pytest.raises(TraitValidationError):
|
||||
empty_file_locations_trait.validate(representation)
|
||||
|
||||
# create valid file locations trait but with not matching sequence
|
||||
# trait
|
||||
representation = Representation(name="test", traits=[
|
||||
FileLocations(file_paths=file_locations_list)
|
||||
])
|
||||
invalid_sequence_trait = FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1051,
|
||||
frames_per_second="25"
|
||||
)
|
||||
|
||||
representation.add_trait(invalid_sequence_trait)
|
||||
with pytest.raises(TraitValidationError):
|
||||
file_locations_trait.validate(representation)
|
||||
|
||||
228
tests/client/ayon_core/pipeline/traits/test_time_traits.py
Normal file
228
tests/client/ayon_core/pipeline/traits/test_time_traits.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
"""Tests for the time related traits."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from ayon_core.pipeline.traits import (
|
||||
FileLocation,
|
||||
FileLocations,
|
||||
FrameRanged,
|
||||
Handles,
|
||||
Representation,
|
||||
Sequence,
|
||||
)
|
||||
from ayon_core.pipeline.traits.trait import TraitValidationError
|
||||
|
||||
|
||||
def test_sequence_validations() -> None:
|
||||
"""Test Sequence trait validation."""
|
||||
file_locations_list = [
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1010 + 1) # because range is zero based
|
||||
]
|
||||
|
||||
file_locations_list += [
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1015, 1020 + 1)
|
||||
]
|
||||
|
||||
file_locations_list += [
|
||||
FileLocation
|
||||
(
|
||||
file_path=Path("/path/to/file.1100.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
]
|
||||
|
||||
representation = Representation(name="test_1", traits=[
|
||||
FileLocations(file_paths=file_locations_list),
|
||||
FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1100, frames_per_second="25"),
|
||||
Sequence(
|
||||
frame_padding=4,
|
||||
frame_spec="1001-1010,1015-1020,1100")
|
||||
])
|
||||
|
||||
representation.get_trait(Sequence).validate(representation)
|
||||
|
||||
# here we set handles and set them as inclusive, so this should pass
|
||||
representation = Representation(name="test_2", traits=[
|
||||
FileLocations(file_paths=[
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1100 + 1) # because range is zero based
|
||||
]),
|
||||
Handles(
|
||||
frame_start_handle=5,
|
||||
frame_end_handle=5,
|
||||
inclusive=True
|
||||
),
|
||||
FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1100, frames_per_second="25"),
|
||||
Sequence(frame_padding=4)
|
||||
])
|
||||
|
||||
representation.validate()
|
||||
|
||||
# do the same but set handles as exclusive
|
||||
representation = Representation(name="test_3", 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, 1105 + 1) # because range is zero based
|
||||
]),
|
||||
Handles(
|
||||
frame_start_handle=5,
|
||||
frame_end_handle=5,
|
||||
inclusive=False
|
||||
),
|
||||
FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1100, frames_per_second="25"),
|
||||
Sequence(frame_padding=4)
|
||||
])
|
||||
|
||||
representation.validate()
|
||||
|
||||
# invalid representation with file range not extended for handles
|
||||
representation = Representation(name="test_4", traits=[
|
||||
FileLocations(file_paths=[
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1050 + 1) # because range is zero based
|
||||
]),
|
||||
Handles(
|
||||
frame_start_handle=5,
|
||||
frame_end_handle=5,
|
||||
inclusive=False
|
||||
),
|
||||
FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1050, frames_per_second="25"),
|
||||
Sequence(frame_padding=4)
|
||||
])
|
||||
|
||||
with pytest.raises(TraitValidationError):
|
||||
representation.validate()
|
||||
|
||||
# invalid representation with frame spec not matching the files
|
||||
del representation
|
||||
representation = Representation(name="test_5", traits=[
|
||||
FileLocations(file_paths=[
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1050 + 1) # because range is zero based
|
||||
]),
|
||||
FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1050, frames_per_second="25"),
|
||||
Sequence(frame_padding=4, frame_spec="1001-1010,1012-2000")
|
||||
])
|
||||
with pytest.raises(TraitValidationError):
|
||||
representation.validate()
|
||||
|
||||
representation = Representation(name="test_6", traits=[
|
||||
FileLocations(file_paths=[
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1050 + 1) # because range is zero based
|
||||
]),
|
||||
Sequence(frame_padding=4, frame_spec="1-1010,1012-1050"),
|
||||
Handles(
|
||||
frame_start_handle=5,
|
||||
frame_end_handle=5,
|
||||
inclusive=False
|
||||
)
|
||||
])
|
||||
with pytest.raises(TraitValidationError):
|
||||
representation.validate()
|
||||
|
||||
representation = Representation(name="test_6", 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_spec="1001-1010,1012-2000"),
|
||||
Handles(
|
||||
frame_start_handle=5,
|
||||
frame_end_handle=5,
|
||||
inclusive=False
|
||||
)
|
||||
])
|
||||
with pytest.raises(TraitValidationError):
|
||||
representation.validate()
|
||||
|
||||
|
||||
|
||||
def test_list_spec_to_frames() -> None:
|
||||
"""Test converting list specification to frames."""
|
||||
assert Sequence.list_spec_to_frames("1-10,20-30,55") == [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 55
|
||||
]
|
||||
assert Sequence.list_spec_to_frames("1,2,3,4,5") == [
|
||||
1, 2, 3, 4, 5
|
||||
]
|
||||
assert Sequence.list_spec_to_frames("1-10") == [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
|
||||
]
|
||||
test_list = list(range(1001, 1011))
|
||||
test_list += list(range(1012, 2001))
|
||||
assert Sequence.list_spec_to_frames("1001-1010,1012-2000") == test_list
|
||||
|
||||
assert Sequence.list_spec_to_frames("1") == [1]
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Invalid frame number in the list: .*"):
|
||||
Sequence.list_spec_to_frames("a")
|
||||
|
||||
|
||||
def test_sequence_get_frame_padding() -> None:
|
||||
"""Test getting frame padding from FileLocations trait."""
|
||||
file_locations_list = [
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1051)
|
||||
]
|
||||
|
||||
representation = Representation(name="test", traits=[
|
||||
FileLocations(file_paths=file_locations_list)
|
||||
])
|
||||
|
||||
assert Sequence.get_frame_padding(
|
||||
file_locations=representation.get_trait(FileLocations)) == 4
|
||||
|
||||
|
|
@ -7,33 +7,32 @@ import pytest
|
|||
from ayon_core.pipeline.traits import (
|
||||
Bundle,
|
||||
FileLocation,
|
||||
FileLocations,
|
||||
FrameRanged,
|
||||
Image,
|
||||
MimeType,
|
||||
Overscan,
|
||||
PixelBased,
|
||||
Planar,
|
||||
Representation,
|
||||
Sequence,
|
||||
TraitBase,
|
||||
)
|
||||
from ayon_core.pipeline.traits.trait import TraitValidationError
|
||||
|
||||
REPRESENTATION_DATA = {
|
||||
FileLocation.id: {
|
||||
"file_path": Path("/path/to/file"),
|
||||
"file_size": 1024,
|
||||
"file_hash": None,
|
||||
"persitent": True,
|
||||
},
|
||||
Image.id: {},
|
||||
Image.id: {"persitent": True},
|
||||
PixelBased.id: {
|
||||
"display_window_width": 1920,
|
||||
"display_window_height": 1080,
|
||||
"pixel_aspect_ratio": 1.0,
|
||||
"persitent": True,
|
||||
},
|
||||
Planar.id: {
|
||||
"planar_configuration": "RGB",
|
||||
"persitent": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -168,6 +167,17 @@ 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."""
|
||||
representation = Representation(name="test")
|
||||
representation[Image.id] = Image()
|
||||
assert Image.id in representation
|
||||
image = representation[Image.id]
|
||||
assert image == Image()
|
||||
for trait_id, trait in representation.items():
|
||||
assert trait_id == Image.id
|
||||
assert trait == Image()
|
||||
|
||||
|
||||
def test_getting_traits_data(representation: Representation) -> None:
|
||||
|
|
@ -194,49 +204,6 @@ def test_traits_data_to_dict(representation: Representation) -> None:
|
|||
assert result == REPRESENTATION_DATA
|
||||
|
||||
|
||||
def test_bundles() -> None:
|
||||
"""Test bundle trait."""
|
||||
diffuse_texture = [
|
||||
Image(),
|
||||
PixelBased(
|
||||
display_window_width=1920,
|
||||
display_window_height=1080,
|
||||
pixel_aspect_ratio=1.0),
|
||||
Planar(planar_configuration="RGB"),
|
||||
FileLocation(
|
||||
file_path=Path("/path/to/diffuse.jpg"),
|
||||
file_size=1024,
|
||||
file_hash=None),
|
||||
MimeType(mime_type="image/jpeg"),
|
||||
]
|
||||
bump_texture = [
|
||||
Image(),
|
||||
PixelBased(
|
||||
display_window_width=1920,
|
||||
display_window_height=1080,
|
||||
pixel_aspect_ratio=1.0),
|
||||
Planar(planar_configuration="RGB"),
|
||||
FileLocation(
|
||||
file_path=Path("/path/to/bump.tif"),
|
||||
file_size=1024,
|
||||
file_hash=None),
|
||||
MimeType(mime_type="image/tiff"),
|
||||
]
|
||||
bundle = Bundle(items=[diffuse_texture, bump_texture])
|
||||
representation = Representation(name="test_bundle", traits=[bundle])
|
||||
|
||||
if representation.contains_trait(trait=Bundle):
|
||||
assert representation.get_trait(trait=Bundle).items == [
|
||||
diffuse_texture, bump_texture
|
||||
]
|
||||
|
||||
for item in representation.get_trait(trait=Bundle).items:
|
||||
sub_representation = Representation(name="test", traits=item)
|
||||
assert sub_representation.contains_trait(trait=Image)
|
||||
assert sub_representation.get_trait(trait=MimeType).mime_type in [
|
||||
"image/jpeg", "image/tiff"
|
||||
]
|
||||
|
||||
def test_get_version_from_id() -> None:
|
||||
"""Test getting version from trait ID."""
|
||||
assert Image().get_version() == 1
|
||||
|
|
@ -334,137 +301,69 @@ def test_from_dict() -> None:
|
|||
"test", trait_data=traits_data)
|
||||
"""
|
||||
|
||||
def test_file_locations_validation() -> None:
|
||||
"""Test FileLocations trait validation."""
|
||||
file_locations_list = [
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1051)
|
||||
]
|
||||
def test_representation_equality() -> None:
|
||||
|
||||
representation = Representation(name="test", traits=[
|
||||
FileLocations(file_paths=file_locations_list)
|
||||
# rep_a and rep_b are equal
|
||||
rep_a = Representation(name="test", traits=[
|
||||
FileLocation(file_path=Path("/path/to/file"), file_size=1024),
|
||||
Image(),
|
||||
PixelBased(
|
||||
display_window_width=1920,
|
||||
display_window_height=1080,
|
||||
pixel_aspect_ratio=1.0),
|
||||
Planar(planar_configuration="RGB"),
|
||||
])
|
||||
rep_b = Representation(name="test", traits=[
|
||||
FileLocation(file_path=Path("/path/to/file"), file_size=1024),
|
||||
Image(),
|
||||
PixelBased(
|
||||
display_window_width=1920,
|
||||
display_window_height=1080,
|
||||
pixel_aspect_ratio=1.0),
|
||||
Planar(planar_configuration="RGB"),
|
||||
])
|
||||
|
||||
file_locations_trait: FileLocations = FileLocations(
|
||||
file_paths=file_locations_list)
|
||||
|
||||
# this should be valid trait
|
||||
file_locations_trait.validate(representation)
|
||||
|
||||
# add valid FrameRanged trait
|
||||
sequence_trait = FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1050,
|
||||
frames_per_second="25"
|
||||
)
|
||||
representation.add_trait(sequence_trait)
|
||||
|
||||
# it should still validate fine
|
||||
file_locations_trait.validate(representation)
|
||||
|
||||
# create empty file locations trait
|
||||
empty_file_locations_trait = FileLocations(file_paths=[])
|
||||
representation = Representation(name="test", traits=[
|
||||
empty_file_locations_trait
|
||||
])
|
||||
with pytest.raises(TraitValidationError):
|
||||
empty_file_locations_trait.validate(representation)
|
||||
|
||||
# create valid file locations trait but with not matching sequence
|
||||
# trait
|
||||
representation = Representation(name="test", traits=[
|
||||
FileLocations(file_paths=file_locations_list)
|
||||
])
|
||||
invalid_sequence_trait = FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1051,
|
||||
frames_per_second="25"
|
||||
)
|
||||
|
||||
representation.add_trait(invalid_sequence_trait)
|
||||
with pytest.raises(TraitValidationError):
|
||||
file_locations_trait.validate(representation)
|
||||
|
||||
def test_sequence_get_frame_padding() -> None:
|
||||
"""Test getting frame padding from FileLocations trait."""
|
||||
file_locations_list = [
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1051)
|
||||
]
|
||||
|
||||
representation = Representation(name="test", traits=[
|
||||
FileLocations(file_paths=file_locations_list)
|
||||
# rep_c has different value for planar_configuration then rep_a and rep_b
|
||||
rep_c = Representation(name="test", traits=[
|
||||
FileLocation(file_path=Path("/path/to/file"), file_size=1024),
|
||||
Image(),
|
||||
PixelBased(
|
||||
display_window_width=1920,
|
||||
display_window_height=1080,
|
||||
pixel_aspect_ratio=1.0),
|
||||
Planar(planar_configuration="RGBA"),
|
||||
])
|
||||
|
||||
assert Sequence.get_frame_padding(
|
||||
file_locations=representation.get_trait(FileLocations)) == 4
|
||||
|
||||
def test_sequence_validations() -> None:
|
||||
"""Test Sequence trait validation."""
|
||||
file_locations_list = [
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1001, 1010 + 1) # because range is zero based
|
||||
]
|
||||
|
||||
file_locations_list += [
|
||||
FileLocation(
|
||||
file_path=Path(f"/path/to/file.{frame}.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
for frame in range(1015, 1020 + 1)
|
||||
]
|
||||
|
||||
file_locations_list += [
|
||||
FileLocation
|
||||
(
|
||||
file_path=Path("/path/to/file.1100.exr"),
|
||||
file_size=1024,
|
||||
file_hash=None,
|
||||
)
|
||||
]
|
||||
|
||||
representation = Representation(name="test", traits=[
|
||||
FileLocations(file_paths=file_locations_list),
|
||||
FrameRanged(
|
||||
frame_start=1001,
|
||||
frame_end=1100, frames_per_second="25"),
|
||||
Sequence(
|
||||
frame_padding=4,
|
||||
frame_spec="1001-1010,1015-1020,1100")
|
||||
rep_d = Representation(name="test", traits=[
|
||||
FileLocation(file_path=Path("/path/to/file"), file_size=1024),
|
||||
Image(),
|
||||
])
|
||||
rep_e = Representation(name="foo", traits=[
|
||||
FileLocation(file_path=Path("/path/to/file"), file_size=1024),
|
||||
Image(),
|
||||
])
|
||||
rep_f = Representation(name="foo", traits=[
|
||||
FileLocation(file_path=Path("/path/to/file"), file_size=1024),
|
||||
Planar(planar_configuration="RGBA"),
|
||||
])
|
||||
|
||||
representation.get_trait(Sequence).validate(representation)
|
||||
|
||||
# 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
|
||||
rep_f.representation_id = rep_a.representation_id
|
||||
assert rep_a == rep_b
|
||||
|
||||
# because of the trait value difference
|
||||
assert rep_a != rep_c
|
||||
# because of the type difference
|
||||
assert rep_a != "foo"
|
||||
# because of the trait count difference
|
||||
assert rep_a != rep_d
|
||||
# because of the name difference
|
||||
assert rep_d != rep_e
|
||||
# because of the trait difference
|
||||
assert rep_d != rep_f
|
||||
|
||||
|
||||
|
||||
|
||||
def test_list_spec_to_frames() -> None:
|
||||
"""Test converting list specification to frames."""
|
||||
assert Sequence.list_spec_to_frames("1-10,20-30,55") == [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 55
|
||||
]
|
||||
assert Sequence.list_spec_to_frames("1,2,3,4,5") == [
|
||||
1, 2, 3, 4, 5
|
||||
]
|
||||
assert Sequence.list_spec_to_frames("1-10") == [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
|
||||
]
|
||||
assert Sequence.list_spec_to_frames("1") == [1]
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Invalid frame number in the list: .*"):
|
||||
Sequence.list_spec_to_frames("a")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue