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:
Ondrej Samohel 2025-02-11 15:52:43 +01:00
commit 279d4bfa6e
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
8 changed files with 97 additions and 59 deletions

View file

@ -163,15 +163,16 @@ class FileLocations(TraitBase):
Args:
representation (Representation): Representation to validate.
Returns:
bool: True if the trait is valid, False otherwise
Raises:
TraitValidationError: If the trait is invalid within the
representation.
"""
super().validate_trait(representation)
if len(self.file_paths) == 0:
# If there are no file paths, we can't validate
msg = "No file locations defined (empty list)"
raise TraitValidationError(self.name, msg)
# 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)
if not representation.contains_trait(Sequence) \
@ -234,7 +235,7 @@ class FileLocations(TraitBase):
raise TraitValidationError(self.name, msg)
if frames_from_spec:
if len(frames_from_spec) != len(self.file_paths) :
if len(frames_from_spec) != len(self.file_paths):
# If the number of file paths does not match the frame range,
# the trait is invalid
msg = (
@ -254,14 +255,14 @@ class FileLocations(TraitBase):
)
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)}) "
"does not match frame range "
f"({length_with_handles})"
)
raise TraitValidationError(self.name, msg)
# 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)}) "
"does not match frame range "
f"({length_with_handles})"
)
raise TraitValidationError(self.name, msg)
frame_ranged: FrameRanged = representation.get_trait(FrameRanged)
@ -281,8 +282,8 @@ class FileLocations(TraitBase):
)
raise TraitValidationError(self.name, msg)
@staticmethod
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.
@ -427,7 +428,12 @@ class Bundle(TraitBase):
..., title="Bundles of traits")
def to_representations(self) -> Generator[Representation]:
"""Convert bundle to representations."""
"""Convert bundle to representations.
Yields:
Representation: Representation of the bundle.
"""
for idx, item in enumerate(self.items):
yield Representation(name=f"{self.name} {idx}", traits=item)

View file

@ -90,6 +90,7 @@ class KeepOriginalLocation(TraitBase):
id: ClassVar[str] = "ayon.meta.KeepOriginalLocation.v1"
persistent: bool = Field(default=False, title="Persistent")
class KeepOriginalName(TraitBase):
"""Keep files in its original name.

View file

@ -44,7 +44,7 @@ def _get_version_from_id(_id: str) -> Optional[int]:
return int(match[1]) if match else None
class Representation(Generic[T]):
class Representation(Generic[T]): # noqa: PLR0904
"""Representation of products.
Representation defines collection of individual properties that describe
@ -54,6 +54,9 @@ class Representation(Generic[T]):
It holds methods to add, remove, get, and check for the existence of a
trait in the representation. It also provides a method to get all the
Note:
`PLR0904` is rule for checking number of public methods in a class.
Arguments:
name (str): Representation name. Must be unique within instance.
representation_id (str): Representation ID.
@ -79,9 +82,6 @@ class Representation(Generic[T]):
Returns:
TraitBase: Trait instance.
Raises:
MissingTraitError: If the trait is not found.
"""
return self.get_trait_by_id(key)
@ -104,8 +104,6 @@ class Representation(Generic[T]):
Args:
key (str): Trait ID.
Raises:
ValueError: If the trait is not found.
"""
self.remove_trait_by_id(key)
@ -138,7 +136,7 @@ class Representation(Generic[T]):
"""Add a trait to the Representation.
Args:
trait (TraiBase): Trait to add.
trait (TraitBase): Trait to add.
exists_ok (bool, optional): If True, do not raise an error if the
trait already exists. Defaults to False.
@ -228,7 +226,6 @@ class Representation(Generic[T]):
for trait_id in trait_ids:
self.remove_trait_by_id(trait_id)
def has_traits(self) -> bool:
"""Check if the Representation has any traits.
@ -260,7 +257,7 @@ class Representation(Generic[T]):
bool: True if the trait exists, False otherwise.
"""
return bool(self._data.get(trait_id))
return bool(self._data.get(trait_id))
def contains_traits(self, traits: list[Type[T]]) -> bool:
"""Check if the traits exist.
@ -366,7 +363,7 @@ class Representation(Generic[T]):
return result
for trait in traits:
result[str(trait.id)] = self.get_trait(trait=trait)
result[str(trait.id)] = self.get_trait(trait=trait)
return result
def get_traits_by_ids(self, trait_ids: list[str]) -> dict[str, T]:
@ -532,12 +529,6 @@ class Representation(Generic[T]):
Returns:
Type[TraitBase]: Trait class.
Raises:
LooseMatchingTraitError: If the trait is found with a loose
matching criteria. This exception will include the trait
class that was found and the expected trait ID. Additional
downstream logic must decide how to handle this error.
"""
version = cls._get_version_from_id(trait_id)
@ -588,9 +579,6 @@ class Representation(Generic[T]):
Raises:
IncompatibleTraitVersionError: If the trait version is incompatible
with the current version of the trait.
UpgradableTraitError: If the trait can upgrade existing data
meant for older versions of the trait.
ValueError: If the trait model with the given ID is not found.
"""
try:
@ -662,6 +650,11 @@ class Representation(Generic[T]):
Returns:
Representation: Representation instance.
Raises:
ValueError: If the trait model with ID is not found.
TypeError: If the trait data is not a dictionary.
IncompatibleTraitVersionError: If the trait version is incompatible
"""
if not trait_data:
trait_data = {}
@ -697,7 +690,6 @@ class Representation(Generic[T]):
return cls(
name=name, representation_id=representation_id, traits=traits)
def validate(self) -> None:
"""Validate the representation.

View file

@ -143,11 +143,20 @@ class Sequence(TraitBase):
def validate_frame_regex(
cls, v: Optional[Pattern]
) -> Optional[Pattern]:
"""Validate frame regex."""
"""Validate frame regex.
Frame regex must have index and padding named groups.
Returns:
Optional[Pattern]: Compiled regex pattern.
Raises:
ValueError: If frame regex does not include 'index' and 'padding'
"""
if v is None:
return v
_v = v.pattern
if v and any(s not in _v for s in ["?P<index>", "?P<padding>"]):
if v and any(s not in v.pattern for s in ["?P<index>", "?P<padding>"]):
msg = "Frame regex must include 'index' and `padding named groups"
raise ValueError(msg)
return v
@ -172,10 +181,6 @@ class Sequence(TraitBase):
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(
@ -309,7 +314,15 @@ class Sequence(TraitBase):
@staticmethod
def list_spec_to_frames(list_spec: str) -> list[int]:
"""Convert list specification to frames."""
"""Convert list specification to frames.
Returns:
list[int]: List of frame numbers.
Raises:
ValueError: If invalid frame number in the list.
"""
frames = []
segments = list_spec.split(",")
for segment in segments:
@ -364,7 +377,12 @@ class Sequence(TraitBase):
@staticmethod
def get_frame_padding(file_locations: FileLocations) -> int:
"""Get frame padding."""
"""Get frame padding.
Returns:
int: Frame padding.
"""
src_collection = Sequence._get_collection(file_locations)
return len(str(max(src_collection.indexes)))
@ -382,6 +400,7 @@ class Sequence(TraitBase):
Default clique pattern is::
\.(?P<index>(?P<padding>0*)\d+)\.\D+\d?$
Returns:
list[int]: List of frame numbers.
@ -394,6 +413,9 @@ class Sequence(TraitBase):
If the regex is string, it will compile it to the pattern.
Returns:
Pattern: Compiled regex pattern.
"""
if self.frame_regex:
if isinstance(self.frame_regex, str):

View file

@ -57,7 +57,7 @@ class TraitBase(ABC, BaseModel):
"""Abstract attribute for description."""
...
def validate_trait(self, representation: Representation) -> None:
def validate_trait(self, representation: Representation) -> None: # noqa: PLR6301
"""Validate the trait.
This method should be implemented in the derived classes to validate
@ -67,10 +67,6 @@ class TraitBase(ABC, BaseModel):
Args:
representation (Representation): Representation instance.
Raises:
TraitValidationError: If the trait is invalid
within representation.
"""
return
@ -82,6 +78,9 @@ class TraitBase(ABC, BaseModel):
This assumes Trait ID ends with `.v{version}`. If not, it will
return None.
Returns:
Optional[int]: Trait version
"""
version_regex = r"v(\d+)$"
match = re.search(version_regex, str(cls.id))
@ -106,7 +105,7 @@ class IncompatibleTraitVersionError(Exception):
"""
class UpgradableTraitError(Generic[T], Exception):
class UpgradableTraitError(Exception, Generic[T]):
"""Upgradable trait version exception.
This exception is raised when the trait can upgrade existing data
@ -118,7 +117,7 @@ class UpgradableTraitError(Generic[T], Exception):
old_data: dict
class LooseMatchingTraitError(Generic[T], Exception):
class LooseMatchingTraitError(Exception, Generic[T]):
"""Loose matching trait exception.
This exception is raised when the trait is found with a loose matching

View file

@ -136,7 +136,15 @@ class UDIM(TraitBase):
@field_validator("udim_regex")
@classmethod
def validate_frame_regex(cls, v: Optional[str]) -> Optional[str]:
"""Validate udim regex."""
"""Validate udim regex.
Returns:
Optional[str]: UDIM regex.
Raises:
ValueError: UDIM regex must include 'udim' named group.
"""
if v is not None and "?P<udim>" not in v:
msg = "UDIM regex must include 'udim' named group"
raise ValueError(msg)

View file

@ -24,6 +24,9 @@ def get_sequence_from_files(paths: list[Path]) -> FrameRanged:
Returns:
FrameRanged: FrameRanged trait.
Raises:
ValueError: If paths cannot be assembled into one collection
"""
cols, rems = assemble([path.as_posix() for path in paths])
if rems:

View file

@ -74,6 +74,7 @@ indent-width = 4
target-version = "py39"
[tool.ruff.lint]
preview = true
pydocstyle.convention = "google"
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
select = ["ALL"]
@ -85,13 +86,19 @@ ignore = [
"S603",
"ERA001",
"TRY003",
"UP006", # support for older python version (type vs. Type)
"UP007", # ..^
"UP035", # ..
"UP006", # support for older python version (type vs. Type)
"UP007", # ..^
"UP035", # ..
"UP045", # Use `X | None` for type annotations
"ARG002",
"INP001", # add `__init__.py` to namespaced package
"FIX002", # FIX all TODOs
"TD003", # missing issue link
"INP001", # add `__init__.py` to namespaced package
"FIX002", # FIX all TODOs
"TD003", # missing issue link
"S404", # subprocess module is possibly insecure
"PLC0415", # import must be on top of the file
"CPY001", # missing copyright header
"UP045"
]
# Allow fix for all enabled rules (when `--fix`) is provided.