Skip to content

Representation defined by traits#979

Closed
antirotor wants to merge 1 commit intodevelopfrom
feature/909-define-basic-trait-type-using-dataclasses
Closed

Representation defined by traits#979
antirotor wants to merge 1 commit intodevelopfrom
feature/909-define-basic-trait-type-using-dataclasses

Conversation

@antirotor
Copy link
Member

@antirotor antirotor commented Oct 31, 2024

Changelog Description

Introducing new way of defining representations. Developer friendly, flexible yet strictly typed, easy to extend using Traits as building blocks defining what representation IS.

Additional info

New representation is holder for traits. Each trait define some properties and together with other traits describes the representation. Traits can be anything- their purpose is to help publisher, loader (and basically any other systems) to clearly identify data needed for processing. Like file system path, or frame per second, or resolution. Those properties are grouped together to logical block - frame range specific, resolution specific, etc.

This PR is introducing several new features:

TraitBase - this is a model for all Traits. It defines abstract properties like id, name, description that needs to be in all Traits.

Note

Every Trait can re-implement validate() method. The one in TraitBase always returns True. Representation is passed to that method so every Trait can validate against all other Trait present in representation.

Representation - this is "container" of sorts to hold all Traits. It holds representationname and id. And lot of "helper" methods to work with Traits:

  • methods to check if Trait exists in the Representation
  • methods to add and remove Traits
  • methods to get Traits
  • method to return whole Representation serialized as Python dict
  • method to reconstruct Representation from the dict

Most of them can run on bulk of Traits and you can access Traits by their class or by their Id. More on that below.

There is also mechanism for upgrading and versioning Traits.

Traits

There are some pre-defined Traits that should come with ayon-core to provide "common language". They are all based on TraitBase and are grouped to logical chunks or namespaces - content, lifecycle, meta, three_dimensional, time, two_dimensional. Their complete list can be found in the code and is obviously subject to change based on the need.

Practical examples

So how to work with traits and representations? To define traits (for example in Collector plugin or in Extractor plugin:

from ayon_core.pipeline.traits import (
	FileLocation,
	Image,,
	MimeType,
    PixelBased,
	Representation
)

# define traits
file_location = FileLocation(
    file_path=Path("/path/to/file.jpg"),
)

pixel_based = PixelBased(
	display_window_width=1920,
    display_window_height=1080,
    pixel_aspect_ratio=1.0 
)

# create representation itself

representation = Representation(
    name="Image test",
	traits=[file_location, Image(), pixel_based])

# add additional traits
mime_type = MimeType(mime_type="image/jpeg")
representation.add_trait(mime_type)

# remove trait
representation.remove_trait(MimeType)

if representation.contains_trait(PixelBased):
	print(f"width: {representation.get_trait(PixelBased).display_window_width)}")

You can work with Traits using classes, but you can also utilize their ids. That is useful when working with representation that was serialized into "plain' dictionary:

# some pseudo-function to get representation as dict
representation = Representation.from_dict(get_representation_dict(...))

# get trait by its id
# note: the use of version in the ID. You can test if trait is present in representation, or if trait of specific version is present.
if representation.contains_trait_by_id("ayon.content.FileLocation"):
	print(f"path: {representation.get_trait_by_id("ayon.content.FileLocation.v1")

# serialize back to dict
representation.traits_as_dict()

There is also feature of version upgrade on Traits. Whenever you want to de-serialize data that are using older version of trait, upgrade() method on newer trait definition is called to reconstruct new version (downgrading isn't possible). So, you can have serialized trait like so (type trait without properties):

{
		ayon.2d.Image.v1: {}
}

But your current runtime has Image trait ayon.2d.Image.v2 that is also adding property foo.

Whenever you run representation.get_trait_by_id("ayon.2d.Image") without version specifier, it can try to find out the latest Trait definition available and if it differs - v1 != v2 it tries to call upgrade() method on the latest trait.

Notes

Traits are no longer Pydantic models because Pydantic 2 is based on rust and we would need to support it in all possible host (distribute it somehow in AYON too). So until this issue is resolved, it is better to have them as pure Python dataclasses.

@antirotor antirotor added type: feature Adding something new and exciting to the product tests PR contains new unit or integration test or improves the existing ones labels Oct 31, 2024
@antirotor antirotor self-assigned this Oct 31, 2024
@antirotor antirotor linked an issue Oct 31, 2024 that may be closed by this pull request
@ynbot ynbot added the size/XXL label Oct 31, 2024
@MustafaJafar
Copy link
Member

Hey, would you like to me do some test runs for this PR?

@antirotor
Copy link
Member Author

antirotor commented Nov 1, 2024

Hey, would you like to me do some test runs for this PR?

no tests yet (apart unit tests that were passing recently). It is more work in progress now - main development is now done on the integrator. This is here to discuss traits and if we have everything we need. I guess more will be clear one we try to use them in existing workflows.

@MustafaJafar
Copy link
Member

I guess more will be clear one we try to use them in existing workflows.

This is most likely how I may test run it by trying to use it in draft PR.
Personally, I find it cool to try new things.

@antirotor
Copy link
Member Author

antirotor commented Nov 6, 2024

Regarding trait versions:

current idea is to store the version in trait id itself - ayon.content.FileLocation.v1 in similar manner OTIO is doing it. Trait have helper function that can parse it from the id - FileLocation.get_version().

Example:

file_location = FileLocation(file_path=Path(...))
# it is class method
file_location.get_version() == FileLocation.get_version()
print(FileLocation.id)
# ayon.content.FileLocation.v1

There are some functions on Representation that takes id without version, then are trying to use the latest available.

I considered having version encoded in id as now, but having it also as @computed_field on the model, but that was somewhat duplicating information.

@antirotor
Copy link
Member Author

Some lingering questions:

Trait inheritance

I am thinking that trait inheritance creates more issues than adds more value. Mainly in following situation:

Currently, Sequence trait is subclass of FrameRanged and Handles. Important bit is, that frames_per_second is defined in FrameRanged. To now, that Sequence has fps because it inherited it from FrameRanged is information I, as developer shouldn't track. I can explicitly check with Representation.contain_trait(FrameRanged) but that will fail because it really has just Sequence. I can of course implement logic for checking subclasses, but I don't like that idea.

class FrameRanged:
    frames_per_second: float
    ...

class Sequence(FrameRanged):
   ...

representation =  Representation(name="test", traits=[
    Sequence(...),
])

# I need to get fps from representation
representation.contain_trait(FrameRanged)   # fails
for trait in representation.get_traits().values:

The other approach I like even less is: iterate over all traits on representation, find first that defines frame_per_second and return it - because that is basically defeating purpose of pydantic models and strong types.

So if there is no inheritance, I can check for specific types and be clear about that, but the tradeoff is that I still need to track what Trait defines that property - which is IMO more straightforward than tracking this AND inheritance.

File sequence

Another question is description of file sequence. There are two ways:

  1. track as a file just first frame, implore the rest based on the fact that other Traits present are marking that representation as a sequence:
# sequence of files
representation = Representation(name="foo", traits=[
	FileLocation(Path("/foo/bar.1001.exr", ...),
	Sequence(frame_start=1001, frame_end=1050, ...)
])

# single file (not sequence of one frame)
representation = Representation(name="foo", traits=[
	FileLocation(Path("/foo/bar.1001.exr", ...),
	Static()
])

Therefor anyone working with such representation needs to check for Sequence and/or Static and consider it in the logic.

  1. be more explicit using trait, that is actually describing all files:
representation = Representation(name="foo", traits=[
	FileLocations(paths=[
		Path("/foo/bar.1001.exr"),
		Path("/foo/bar.1002.exr"),
		...
	]),
	Sequence(frame_start=1001, frame_end=1050, ...)
])

With this, we can track explicitly all individual files, we don't need to do all those calculations of possible frame padding, etc. and the traits can use validate() logic on them (so Sequence can check that FileLocations has matching files and vice-versa).

FrameRanged: FrameRanged trait.

"""
col = assemble([path.as_posix() for path in paths])[0][0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will error on:

  • File paths that do not have a sequence identifier
  • File paths that only contain a single frame

Because both won'tbe detected as collections but as remainder

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this function is potential danger, because it sounds like it can be used for any case, but it causing the same restriction we have now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main use of this function is on FrameRanged trait validation that should be sequence by default and if it fails then it's just right thing to do. But maybe it should be better annotated then.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a new representation model defined by traits that makes data handling more flexible, strictly typed, and developer friendly. Key changes include:

  • New trait implementations spanning content, time, two-/three-dimensional, cryptography, and lifecycle domains.
  • A comprehensive set of tests to validate trait functionality and representation behavior.
  • Updates to configuration files (pyproject.toml, addon interfaces) to support improved linting, type checking, and addon structure.

Reviewed Changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/client/ayon_core/pipeline/traits/test_two_dimesional_traits.py New tests for 2D traits including UDIM and file location functionality.
tests/client/ayon_core/pipeline/traits/test_traits.py Tests ensuring representation and trait equality, addition, and removal work as expected.
tests/client/ayon_core/pipeline/traits/test_time_traits.py Validation tests for time-related traits and frame range parsing.
tests/client/ayon_core/pipeline/traits/test_content_traits.py Tests for content traits including bundle handling and file locations validation.
pyproject.toml Updates to linting (ruff, pre-commit) and type checking (mypy) configurations.
client/ayon_core/pipeline/traits/*.py New trait model implementations and utility functions for representation.
client/ayon_core/addon/interfaces.py and init.py Minor adjustments to addon interfaces and initialization for better type hints.
Comments suppressed due to low confidence (1)

tests/client/ayon_core/pipeline/traits/test_two_dimesional_traits.py:1

  • The file name 'test_two_dimesional_traits.py' appears to have a typo; consider renaming it to 'test_two_dimensional_traits.py' for clarity and consistency.
Filename: test_two_dimesional_traits.py

@antirotor
Copy link
Member Author

This was already merged in #1147

@antirotor antirotor closed this Aug 27, 2025
@github-project-automation github-project-automation bot moved this from Review In Progress to Done in PR reviewing Aug 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XXL tests PR contains new unit or integration test or improves the existing ones type: feature Adding something new and exciting to the product

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Define basic trait type using dataclasses

5 participants

Comments