Representation traits: hero version integrator#1766
Conversation
…rating-hero-version-not-using-all-representations
…rsion-not-using-all-representations' into 1685-yn-0470-integrating-hero-version-not-using-all-representations
…-using-all-representations
…-using-all-representations
…-using-all-representations
There was a problem hiding this comment.
Pull request overview
Adds trait-focused publishing helpers and introduces a new hero-version integrator for trait-based representations, alongside refactors to reuse the new helpers and broaden supported file-layout cases (shared common root with preserved leaf hierarchy).
Changes:
- Added
ayon_core.pipeline.traits.publishingmodule to centralize transfer-building, template data, and legacy file info logic. - Added
IntegrateHeroVersionTraitsplugin to create/replace hero versions for trait-based representations. - Expanded
FileLocationsvalidation/integration to allow multi-file representations without Sequence/UDIM when files share a meaningful common root; added unit tests.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/client/ayon_core/plugins/publish/test_integrate_traits.py | Updated tests to use the new publishing helper functions and added a hierarchy-preservation test. |
| tests/client/ayon_core/pipeline/traits/test_publishing.py | New unit tests for transfer generation across single-file/sequence/UDIM/common-root/bundle cases. |
| tests/client/ayon_core/pipeline/traits/test_content_traits.py | Updated validation test to reflect new “common root” allowance for FileLocations. |
| client/ayon_core/plugins/publish/integrate_traits.py | Refactored integrator to use centralized helper functions from traits publishing module. |
| client/ayon_core/plugins/publish/integrate_hero_version_with_traits.py | New hero-version integrator for trait-based representations. |
| client/ayon_core/plugins/publish/integrate_hero_version.py | Minor typing adjustments. |
| client/ayon_core/pipeline/traits/utils.py | Removed/moved sequence detection helper from utils. |
| client/ayon_core/pipeline/traits/publishing.py | New shared logic for building transfers, template data, and legacy file dictionaries. |
| client/ayon_core/pipeline/traits/content.py | Added FileLocations.get_common_root() and updated validation to permit common-root hierarchies; added OriginalFilename trait. |
| client/ayon_core/pipeline/traits/init.py | Re-exported new publishing helpers and TransferItem. |
| client/ayon_core/pipeline/publish/lib.py | Exposed template/version/rootless helper utilities and TemplateItem for broader reuse. |
| client/ayon_core/pipeline/publish/init.py | Re-exported new helpers from publish library. |
| client/ayon_core/pipeline/anatomy/templates.py | Added typing to get_template_item. |
| client/ayon_core/pipeline/anatomy/anatomy.py | Added typed get_template_item wrapper (but changes default-missing behavior). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
BigRoy
left a comment
There was a problem hiding this comment.
The amount of code here is massive :')
But overall seems ok, commented on some code bits mostly.
| from .exceptions import RootCombinationError, ProjectNotSet | ||
| from .roots import AnatomyRoots | ||
| from .templates import AnatomyTemplates | ||
| from .templates import AnatomyTemplates, PLACEHOLDER |
There was a problem hiding this comment.
I have my doubts about importing this placeholder object from templates. Seems a bit odd. @iLLiCiTiT thoughts?
There was a problem hiding this comment.
You are right there, but I couldn't quickly think of another solution. PLACEHOLDER is used there as a guard and while my brain was broken about all the arguments of get_template_item and stuff I wanted to expose the real function arguments downstream without rewriting the whole thing. Maybe there is a better way.
There was a problem hiding this comment.
Then rename it, PLACEHOLDER is a "placeholder", if it's being made public it has to make sense, something like NOT_SET.
| return anatomy.get_template_item("publish", template_name) | ||
|
|
||
|
|
||
| def get_instance_families(instance: pyblish.api.Instance) -> list[str]: |
There was a problem hiding this comment.
May want to make regular Integrator start using this as well then?
There was a problem hiding this comment.
definitely, but I think this should be done in another PR
| return families | ||
|
|
||
|
|
||
| def get_version_data_from_instance( |
There was a problem hiding this comment.
Should probably make regular Integrator use this as well, to ensure both approaches are the same, no? Especially because this doesn't seem representation traits specific?
There was a problem hiding this comment.
again, definitely, but I think this should be done in another PR (I'll copy paste it to all similar :) - I get your point but even for testing it would be better to have it in separate PR)
There was a problem hiding this comment.
Me being technicall, the get_ probably should be prepare_ as it also changes data on the instance.
| return version_data | ||
|
|
||
|
|
||
| def get_rootless_path(anatomy: "Anatomy", path: str) -> str: |
There was a problem hiding this comment.
Probably also use in regular integrator?
There was a problem hiding this comment.
To be honest I don't see any advantage of having this function. I see it being used to get source, I would rather have get_instance_source instead of this.
EDITED:
I see it is also being used for representation entity files, but that one probably should not gracefully use path but force to use rootless path.
| sorted_frames = sorted(col.indexes) | ||
| # First frame used for end value | ||
| first_frame = sorted_frames[0] | ||
| # Get last frame for padding |
There was a problem hiding this comment.
Comment is invalid - it's not use for padding?
| Attributes: | ||
| name (str): Trait name. | ||
| description (str): Trait description. | ||
| id (str): id should be namespaced trait name with version |
There was a problem hiding this comment.
Should persistent be in docstring too?
| This expects the file to exist - it must run after the transfer | ||
| is done. |
There was a problem hiding this comment.
May be good to describe here why we need the legacy files still?
| if representation.contains_trait(FileLocations): | ||
| # If representation has FileLocations trait (list of files) | ||
| # it can be a Sequence, UDIM tile set, or a group of related | ||
| # files that share a common root and preserve their hierarchy. | ||
| # Note: we do not support yet frame sequence of multiple UDIM | ||
| # tiles in the same representation. | ||
| get_transfers_from_file_locations( | ||
| representation, template_item, transfers | ||
| ) | ||
| elif representation.contains_trait(FileLocation): | ||
| # This is just a single file representation |
There was a problem hiding this comment.
Does that mean that if FileLocation trait is set but also FileLocations that FileLocation is ignored? Should this be validated/warned upon somewhere? or are we already doing that?
Same for Bundle btw.
| # treat Variant as `output` in template data | ||
| with contextlib.suppress(MissingTraitError): | ||
| template_data["output"] = ( | ||
| representation.get_trait(Variant).variant | ||
| ) |
There was a problem hiding this comment.
I don't think the assumption that variant is the output is correct, right? I assume output here is what outputName was supposed to be? But a single instance (hence one variant) can have multiple output names.
Or is the fact that Representations have "Variant" traits just misleading/confusing and do have mixed terminology here between Variants for products and Variants for representations now?
BigRoy
left a comment
There was a problem hiding this comment.
Didn't test but aside of the still pending comments, code changes seem fine.
…rating-hero-version-not-using-all-representations
|
Can someone resolve the conflicts. |
| return path | ||
|
|
||
|
|
||
| class TemplateItem: |
There was a problem hiding this comment.
This is very confusing a TemplateItem holding AnatomyTemplateItem, which in fact is TemplateItem... I wonder if you ever tried to find out where classes are being used... This is maintanance hell.
I would move it to traits as it has no other use, and rename it to something like IntegrateTemplateItem.
Is there a reason to have both template and template_object? You could just have the object and get the template from it...
| template_name: str, | ||
| subkey: Optional[str] = None, | ||
| default: Any = PLACEHOLDER, | ||
| ) -> Union[TemplateItem, str, None]: |
There was a problem hiding this comment.
How it can return str? It either returns TemplateItem or Any if default is passed in.
| ) -> Union[TemplateItem, str, None]: | |
| ) -> TemplateItem | Any: |
| get_publish_template_object, | ||
| get_instance_families, | ||
| get_version_data_from_instance, | ||
| get_rootless_path, |
There was a problem hiding this comment.
I would put instance to each of the functions (get_instance_template_name, get_instance_publish_template). We should easily distinct these helper from "the" implementations.
| ).hexdigest() | ||
|
|
||
|
|
||
| def get_publish_template_object( |
There was a problem hiding this comment.
Do we have the same function twice now?
| self.related_trait = related_trait | ||
|
|
||
| @staticmethod | ||
| def get_size(file_path: Path) -> int: |
There was a problem hiding this comment.
Why are these helper methods on the class? If it's because it is directly related to them then rename it to get_file_size and get_file_checksum as I would expect that get_size would return what self.size value is. We should also have a way how to get infromation about which checksum type was used (sha256).
|
|
||
|
|
||
| @dataclass | ||
| class OriginalFilename(TraitBase): |
There was a problem hiding this comment.
I wonder what this trait is about? When do we set this? What is usage of it? Because we do define this based on template, and it might be partial, we technically do allow to have template like:
- directory:
{originalDirname}/publish - file:
{originalFilename}
There was a problem hiding this comment.
The idea is that this trait is set by the integrator if the file names needs to remain in case where there are multiple files in forking paths. Until we support it directly in templates, this is at least flag to know that it happened.
| for file_location in self.file_paths: | ||
| yield file_location.file_path | ||
|
|
||
| def get_common_root(self) -> Path: |
There was a problem hiding this comment.
What is the use-case of this again? How can one representation have multiple files in different directories?
There was a problem hiding this comment.
With traits, it can. Under the common root. If that's the case, integrator will apply template formatting up to the common root. If we ever need to go further, we would need to add some "globing" functionality to templates..,
|
Why we don't integrate traits within integrate hero plugin? |
@antirotor can you loop back here? |
…rating-hero-version-not-using-all-representations
…rating-hero-version-not-using-all-representations


Changelog Description
Adding hero version integrator and re-organizing code for publishing traits. Added possibility to integrate file paths sharing common root but living under different leaf folders where the anatomy template is used just for the common part. Added more unit tests for publishing traits.
Additional info
This adds hero version integrator for trait-based representations. It is a little bit simpler, basically replacing the whole hero version with the new one, It tries to follow the logic of the "standard" integrators but during writing this I realized, we should really simplify the logic, get rid of all the exceptions and make it more api (and therefor testing) friendly.
One additional change adds possibility to integrate representation made up by paths pointing to files in different directories but sharing the same root. For example:
in this case the template would be applied to
/foo/barand thebaz/file.1andgoo/file.2will be copied there as they are. Note that this won't change the frame padding, or do re-numbering of the files when needed - so in that sense it is not functionally on par with how the representations are integrated right now, that functionality has to be added later when needed.Testing notes:
Note: I'll link simple testing branch for Maya that I used to publish pointcaches as trait-based representations. Currently only Marvelous Designer is using them.