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:
Ondřej Samohel 2024-11-30 00:50:14 +01:00
commit ce62f7d152
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
7 changed files with 699 additions and 221 deletions

View file

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

View file

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

View file

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

View file

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

View 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)

View 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

View file

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