From 04a2f08fea82ce4f546f673d4859f6a6116712ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Apr 2025 18:05:54 +0200 Subject: [PATCH 001/108] Added new Pre and Post Loader Hooks Added functions to register/deregister them. --- client/ayon_core/pipeline/__init__.py | 24 ++++++++ client/ayon_core/pipeline/load/__init__.py | 24 ++++++++ client/ayon_core/pipeline/load/plugins.py | 66 ++++++++++++++++++++++ 3 files changed, 114 insertions(+) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index 41bcd0dbd1..c6903f1b8e 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -42,6 +42,18 @@ from .load import ( register_loader_plugin_path, deregister_loader_plugin, + discover_loader_pre_hook_plugin, + register_loader_pre_hook_plugin, + deregister_loader_pre_hook_plugin, + register_loader_pre_hook_plugin_path, + deregister_loader_pre_hook_plugin_path, + + discover_loader_post_hook_plugin, + register_loader_post_hook_plugin, + deregister_loader_post_hook_plugin, + register_loader_post_hook_plugin_path, + deregister_loader_post_hook_plugin_path, + load_container, remove_container, update_container, @@ -160,6 +172,18 @@ __all__ = ( "register_loader_plugin_path", "deregister_loader_plugin", + "discover_loader_pre_hook_plugin", + "register_loader_pre_hook_plugin", + "deregister_loader_pre_hook_plugin", + "register_loader_pre_hook_plugin_path", + "deregister_loader_pre_hook_plugin_path", + + "discover_loader_post_hook_plugin", + "register_loader_post_hook_plugin", + "deregister_loader_post_hook_plugin", + "register_loader_post_hook_plugin_path", + "deregister_loader_post_hook_plugin_path", + "load_container", "remove_container", "update_container", diff --git a/client/ayon_core/pipeline/load/__init__.py b/client/ayon_core/pipeline/load/__init__.py index bdc5ece620..fdbcaec1b9 100644 --- a/client/ayon_core/pipeline/load/__init__.py +++ b/client/ayon_core/pipeline/load/__init__.py @@ -49,6 +49,18 @@ from .plugins import ( deregister_loader_plugin_path, register_loader_plugin_path, deregister_loader_plugin, + + discover_loader_pre_hook_plugin, + register_loader_pre_hook_plugin, + deregister_loader_pre_hook_plugin, + register_loader_pre_hook_plugin_path, + deregister_loader_pre_hook_plugin_path, + + discover_loader_post_hook_plugin, + register_loader_post_hook_plugin, + deregister_loader_post_hook_plugin, + register_loader_post_hook_plugin_path, + deregister_loader_post_hook_plugin_path, ) @@ -103,4 +115,16 @@ __all__ = ( "deregister_loader_plugin_path", "register_loader_plugin_path", "deregister_loader_plugin", + + "discover_loader_pre_hook_plugin", + "register_loader_pre_hook_plugin", + "deregister_loader_pre_hook_plugin", + "register_loader_pre_hook_plugin_path", + "deregister_loader_pre_hook_plugin_path", + + "discover_loader_post_hook_plugin", + "register_loader_post_hook_plugin", + "deregister_loader_post_hook_plugin", + "register_loader_post_hook_plugin_path", + "deregister_loader_post_hook_plugin_path", ) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index b601914acd..553e88c2f7 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,5 +1,6 @@ import os import logging +from typing import ClassVar from ayon_core.settings import get_project_settings from ayon_core.pipeline.plugin_discover import ( @@ -264,6 +265,30 @@ class ProductLoaderPlugin(LoaderPlugin): """ +class PreLoadHookPlugin: + """Plugin that should be run before any Loaders in 'loaders' + + Should be used as non-invasive method to enrich core loading process. + Any external studio might want to modify loaded data before or afte + they are loaded without need to override existing core plugins. + """ + loaders: ClassVar[set[str]] + + def process(self, context, name=None, namespace=None, options=None): + pass + +class PostLoadHookPlugin: + """Plugin that should be run after any Loaders in 'loaders' + + Should be used as non-invasive method to enrich core loading process. + Any external studio might want to modify loaded data before or afte + they are loaded without need to override existing core plugins. + loaders: ClassVar[set[str]] + """ + def process(self, context, name=None, namespace=None, options=None): + pass + + def discover_loader_plugins(project_name=None): from ayon_core.lib import Logger from ayon_core.pipeline import get_current_project_name @@ -300,3 +325,44 @@ def deregister_loader_plugin_path(path): def register_loader_plugin_path(path): return register_plugin_path(LoaderPlugin, path) + + +def discover_loader_pre_hook_plugin(project_name=None): + plugins = discover(PreLoadHookPlugin) + return plugins + +def register_loader_pre_hook_plugin(plugin): + return register_plugin(PreLoadHookPlugin, plugin) + + +def deregister_loader_pre_hook_plugin(plugin): + deregister_plugin(PreLoadHookPlugin, plugin) + + +def register_loader_pre_hook_plugin_path(path): + return register_plugin_path(PreLoadHookPlugin, path) + + +def deregister_loader_pre_hook_plugin_path(path): + deregister_plugin_path(PreLoadHookPlugin, path) + + +def discover_loader_post_hook_plugin(): + plugins = discover(PostLoadHookPlugin) + return plugins + + +def register_loader_post_hook_plugin(plugin): + return register_plugin(PostLoadHookPlugin, plugin) + + +def deregister_loader_post_hook_plugin(plugin): + deregister_plugin(PostLoadHookPlugin, plugin) + + +def register_loader_post_hook_plugin_path(path): + return register_plugin_path(PostLoadHookPlugin, path) + + +def deregister_loader_post_hook_plugin_path(path): + deregister_plugin_path(PostLoadHookPlugin, path) From 7ec99b3715e800643a373eb20ccc9df7301ca2ec Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Apr 2025 18:06:31 +0200 Subject: [PATCH 002/108] Initial implementation of pre/post loader hooks --- .../ayon_core/tools/loader/models/actions.py | 110 ++++++++++++++++-- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index cfe91cadab..c188bf1609 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -9,6 +9,7 @@ import ayon_api from ayon_core.lib import NestedCacheItem from ayon_core.pipeline.load import ( discover_loader_plugins, + discover_loader_pre_hook_plugin, ProductLoaderPlugin, filter_repre_contexts_by_loader, get_loader_identifier, @@ -17,6 +18,8 @@ from ayon_core.pipeline.load import ( load_with_product_contexts, LoadError, IncompatibleLoaderError, + get_loaders_by_name + ) from ayon_core.tools.loader.abstract import ActionItem @@ -50,6 +53,8 @@ class LoaderActionsModel: levels=1, lifetime=self.loaders_cache_lifetime) self._repre_loaders = NestedCacheItem( levels=1, lifetime=self.loaders_cache_lifetime) + self._hook_loaders_by_identifier = NestedCacheItem( + levels=1, lifetime=self.loaders_cache_lifetime) def reset(self): """Reset the model with all cached items.""" @@ -58,6 +63,7 @@ class LoaderActionsModel: self._loaders_by_identifier.reset() self._product_loaders.reset() self._repre_loaders.reset() + self._hook_loaders_by_identifier.reset() def get_versions_action_items(self, project_name, version_ids): """Get action items for given version ids. @@ -143,12 +149,14 @@ class LoaderActionsModel: ACTIONS_MODEL_SENDER, ) loader = self._get_loader_by_identifier(project_name, identifier) + hooks = self._get_hook_loaders_by_identifier(project_name, identifier) if representation_ids is not None: error_info = self._trigger_representation_loader( loader, options, project_name, representation_ids, + hooks ) elif version_ids is not None: error_info = self._trigger_version_loader( @@ -156,6 +164,7 @@ class LoaderActionsModel: options, project_name, version_ids, + hooks ) else: raise NotImplementedError( @@ -307,22 +316,40 @@ class LoaderActionsModel: we want to show loaders for? Returns: - tuple[list[ProductLoaderPlugin], list[LoaderPlugin]]: Discovered - loader plugins. + tuple( + list[ProductLoaderPlugin], + list[LoaderPlugin], + ): Discovered loader plugins. """ loaders_by_identifier_c = self._loaders_by_identifier[project_name] product_loaders_c = self._product_loaders[project_name] repre_loaders_c = self._repre_loaders[project_name] + hook_loaders_by_identifier_c = self._hook_loaders_by_identifier[project_name] if loaders_by_identifier_c.is_valid: - return product_loaders_c.get_data(), repre_loaders_c.get_data() + return ( + product_loaders_c.get_data(), + repre_loaders_c.get_data(), + hook_loaders_by_identifier_c.get_data() + ) # Get all representation->loader combinations available for the # index under the cursor, so we can list the user the options. available_loaders = self._filter_loaders_by_tool_name( project_name, discover_loader_plugins(project_name) ) - + hook_loaders_by_identifier = {} + pre_load_hook_plugins = discover_loader_pre_hook_plugin(project_name) + loaders_by_name = get_loaders_by_name() + for hook_plugin in pre_load_hook_plugins: + for load_plugin_name in hook_plugin.loaders: + load_plugin = loaders_by_name.get(load_plugin_name) + if not load_plugin: + continue + if not load_plugin.enabled: + continue + identifier = get_loader_identifier(load_plugin) + hook_loaders_by_identifier.setdefault(identifier, {}).setdefault("pre", []).append(hook_plugin) repre_loaders = [] product_loaders = [] loaders_by_identifier = {} @@ -340,6 +367,8 @@ class LoaderActionsModel: loaders_by_identifier_c.update_data(loaders_by_identifier) product_loaders_c.update_data(product_loaders) repre_loaders_c.update_data(repre_loaders) + hook_loaders_by_identifier_c.update_data(hook_loaders_by_identifier) + return product_loaders, repre_loaders def _get_loader_by_identifier(self, project_name, identifier): @@ -349,6 +378,13 @@ class LoaderActionsModel: loaders_by_identifier = loaders_by_identifier_c.get_data() return loaders_by_identifier.get(identifier) + def _get_hook_loaders_by_identifier(self, project_name, identifier): + if not self._hook_loaders_by_identifier[project_name].is_valid: + self._get_loaders(project_name) + hook_loaders_by_identifier_c = self._hook_loaders_by_identifier[project_name] + hook_loaders_by_identifier_c = hook_loaders_by_identifier_c.get_data() + return hook_loaders_by_identifier_c.get(identifier) + def _actions_sorter(self, action_item): """Sort the Loaders by their order and then their name. @@ -606,6 +642,7 @@ class LoaderActionsModel: options, project_name, version_ids, + hooks=None ): """Trigger version loader. @@ -655,7 +692,7 @@ class LoaderActionsModel: }) return self._load_products_by_loader( - loader, product_contexts, options + loader, product_contexts, options, hooks=hooks ) def _trigger_representation_loader( @@ -664,6 +701,7 @@ class LoaderActionsModel: options, project_name, representation_ids, + hooks ): """Trigger representation loader. @@ -716,10 +754,16 @@ class LoaderActionsModel: }) return self._load_representations_by_loader( - loader, repre_contexts, options + loader, repre_contexts, options, hooks ) - def _load_representations_by_loader(self, loader, repre_contexts, options): + def _load_representations_by_loader( + self, + loader, + repre_contexts, + options, + hooks=None + ): """Loops through list of repre_contexts and loads them with one loader Args: @@ -737,12 +781,26 @@ class LoaderActionsModel: if version < 0: version = "Hero" try: + for hook_plugin in hooks.get("pre", []): + hook_plugin.process( + loader, + repre_context, + options=options + ) + load_with_repre_context( loader, repre_context, options=options ) + for hook_plugin in hooks.get("post", []): + hook_plugin.process( + loader, + repre_context, + options=options + ) + except IncompatibleLoaderError as exc: print(exc) error_info.append(( @@ -770,7 +828,13 @@ class LoaderActionsModel: )) return error_info - def _load_products_by_loader(self, loader, version_contexts, options): + def _load_products_by_loader( + self, + loader, + version_contexts, + options, + hooks=None + ): """Triggers load with ProductLoader type of loaders. Warning: @@ -791,12 +855,26 @@ class LoaderActionsModel: product_name = context.get("product", {}).get("name") or "N/A" product_names.append(product_name) try: + for hook_plugin in hooks.get("pre", []): + hook_plugin.process( + loader, + version_contexts, + options=options + ) + load_with_product_contexts( loader, version_contexts, - options=options + options=options, ) + for hook_plugin in hooks.get("post", []): + hook_plugin.process( + loader, + version_contexts, + options=options + ) + except Exception as exc: formatted_traceback = None if not isinstance(exc, LoadError): @@ -817,12 +895,26 @@ class LoaderActionsModel: version_context.get("product", {}).get("name") or "N/A" ) try: + for hook_plugin in hooks.get("pre", []): + hook_plugin.process( + loader, + version_contexts, + options=options + ) + load_with_product_context( loader, version_context, options=options ) + for hook_plugin in hooks.get("post", []): + hook_plugin.process( + loader, + version_context, + options=options + ) + except Exception as exc: formatted_traceback = None if not isinstance(exc, LoadError): From e76691ea6e62473a923e9ae576d161a418f5e288 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 15:39:32 +0200 Subject: [PATCH 003/108] Moved usage of hooks out of UI actions to utils It is assumed that utils could be used in some kind 'load API', `actions` are tightly bound to UI loading. --- client/ayon_core/pipeline/load/utils.py | 74 +++++++++++++++++-- .../ayon_core/tools/loader/models/actions.py | 49 ++---------- 2 files changed, 72 insertions(+), 51 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index de8e1676e7..d280aa3407 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -288,7 +288,13 @@ def get_representation_context(project_name, representation): def load_with_repre_context( - Loader, repre_context, namespace=None, name=None, options=None, **kwargs + Loader, + repre_context, + namespace=None, + name=None, + options=None, + hooks=None, + **kwargs ): # Ensure the Loader is compatible for the representation @@ -322,11 +328,24 @@ def load_with_repre_context( # Deprecated - to be removed in OpenPype 3.16.6 or 3.17.0. loader._fname = get_representation_path_from_context(repre_context) - return loader.load(repre_context, name, namespace, options) + return _load_context( + Loader, + repre_context, + name, + namespace, + options, + hooks + ) def load_with_product_context( - Loader, product_context, namespace=None, name=None, options=None, **kwargs + Loader, + product_context, + namespace=None, + name=None, + options=None, + hooks=None, + **kwargs ): # Ensure options is a dictionary when no explicit options provided @@ -344,12 +363,24 @@ def load_with_product_context( Loader.__name__, product_context["folder"]["path"] ) ) - - return Loader().load(product_context, name, namespace, options) + return _load_context( + Loader, + product_context, + name, + namespace, + options, + hooks + ) def load_with_product_contexts( - Loader, product_contexts, namespace=None, name=None, options=None, **kwargs + Loader, + product_contexts, + namespace=None, + name=None, + options=None, + hooks=None, + **kwargs ): # Ensure options is a dictionary when no explicit options provided @@ -371,8 +402,37 @@ def load_with_product_contexts( Loader.__name__, joined_product_names ) ) + return _load_context( + Loader, + product_contexts, + name, + namespace, + options, + hooks + ) - return Loader().load(product_contexts, name, namespace, options) + +def _load_context(Loader, contexts, hooks, name, namespace, options): + """Helper function to wrap hooks around generic load function. + + Only dynamic part is different context(s) to be loaded. + """ + for hook_plugin in hooks.get("pre", []): + hook_plugin.process( + contexts, + name, + namespace, + options, + ) + load_return = Loader().load(contexts, name, namespace, options) + for hook_plugin in hooks.get("post", []): + hook_plugin.process( + contexts, + name, + namespace, + options, + ) + return load_return def load_container( diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index c188bf1609..7151dbdaec 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -781,26 +781,14 @@ class LoaderActionsModel: if version < 0: version = "Hero" try: - for hook_plugin in hooks.get("pre", []): - hook_plugin.process( - loader, - repre_context, - options=options - ) load_with_repre_context( loader, repre_context, - options=options + options=options, + hooks=hooks ) - for hook_plugin in hooks.get("post", []): - hook_plugin.process( - loader, - repre_context, - options=options - ) - except IncompatibleLoaderError as exc: print(exc) error_info.append(( @@ -855,26 +843,12 @@ class LoaderActionsModel: product_name = context.get("product", {}).get("name") or "N/A" product_names.append(product_name) try: - for hook_plugin in hooks.get("pre", []): - hook_plugin.process( - loader, - version_contexts, - options=options - ) - load_with_product_contexts( loader, version_contexts, options=options, + hooks=hooks ) - - for hook_plugin in hooks.get("post", []): - hook_plugin.process( - loader, - version_contexts, - options=options - ) - except Exception as exc: formatted_traceback = None if not isinstance(exc, LoadError): @@ -895,26 +869,13 @@ class LoaderActionsModel: version_context.get("product", {}).get("name") or "N/A" ) try: - for hook_plugin in hooks.get("pre", []): - hook_plugin.process( - loader, - version_contexts, - options=options - ) - load_with_product_context( loader, version_context, - options=options + options=options, + hooks=hooks ) - for hook_plugin in hooks.get("post", []): - hook_plugin.process( - loader, - version_context, - options=options - ) - except Exception as exc: formatted_traceback = None if not isinstance(exc, LoadError): From f10564ffb83dce99bd623aa10b5cb622f6d9ffa5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 16:25:59 +0200 Subject: [PATCH 004/108] Removed unnecessary functions --- client/ayon_core/pipeline/load/__init__.py | 4 ---- client/ayon_core/pipeline/load/plugins.py | 9 --------- 2 files changed, 13 deletions(-) diff --git a/client/ayon_core/pipeline/load/__init__.py b/client/ayon_core/pipeline/load/__init__.py index fdbcaec1b9..7f9734f94e 100644 --- a/client/ayon_core/pipeline/load/__init__.py +++ b/client/ayon_core/pipeline/load/__init__.py @@ -50,13 +50,11 @@ from .plugins import ( register_loader_plugin_path, deregister_loader_plugin, - discover_loader_pre_hook_plugin, register_loader_pre_hook_plugin, deregister_loader_pre_hook_plugin, register_loader_pre_hook_plugin_path, deregister_loader_pre_hook_plugin_path, - discover_loader_post_hook_plugin, register_loader_post_hook_plugin, deregister_loader_post_hook_plugin, register_loader_post_hook_plugin_path, @@ -116,13 +114,11 @@ __all__ = ( "register_loader_plugin_path", "deregister_loader_plugin", - "discover_loader_pre_hook_plugin", "register_loader_pre_hook_plugin", "deregister_loader_pre_hook_plugin", "register_loader_pre_hook_plugin_path", "deregister_loader_pre_hook_plugin_path", - "discover_loader_post_hook_plugin", "register_loader_post_hook_plugin", "deregister_loader_post_hook_plugin", "register_loader_post_hook_plugin_path", diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 553e88c2f7..6b1591221a 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -327,10 +327,6 @@ def register_loader_plugin_path(path): return register_plugin_path(LoaderPlugin, path) -def discover_loader_pre_hook_plugin(project_name=None): - plugins = discover(PreLoadHookPlugin) - return plugins - def register_loader_pre_hook_plugin(plugin): return register_plugin(PreLoadHookPlugin, plugin) @@ -347,11 +343,6 @@ def deregister_loader_pre_hook_plugin_path(path): deregister_plugin_path(PreLoadHookPlugin, path) -def discover_loader_post_hook_plugin(): - plugins = discover(PostLoadHookPlugin) - return plugins - - def register_loader_post_hook_plugin(plugin): return register_plugin(PostLoadHookPlugin, plugin) From 92600726b3e9e08a5f3f7f7d63d3919cf436b011 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 16:27:50 +0200 Subject: [PATCH 005/108] Extracted get_hook_loaders_by_identifier to utils --- client/ayon_core/pipeline/load/__init__.py | 2 ++ client/ayon_core/pipeline/load/utils.py | 32 +++++++++++++++++++ .../ayon_core/tools/loader/models/actions.py | 16 ++-------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/load/__init__.py b/client/ayon_core/pipeline/load/__init__.py index 7f9734f94e..eaba8cd78d 100644 --- a/client/ayon_core/pipeline/load/__init__.py +++ b/client/ayon_core/pipeline/load/__init__.py @@ -24,6 +24,7 @@ from .utils import ( get_loader_identifier, get_loaders_by_name, + get_hook_loaders_by_identifier, get_representation_path_from_context, get_representation_path, @@ -89,6 +90,7 @@ __all__ = ( "get_loader_identifier", "get_loaders_by_name", + "get_hook_loaders_by_identifier", "get_representation_path_from_context", "get_representation_path", diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index d280aa3407..9d3635a186 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -16,6 +16,7 @@ from ayon_core.lib import ( ) from ayon_core.pipeline import ( Anatomy, + discover ) log = logging.getLogger(__name__) @@ -1149,3 +1150,34 @@ def filter_containers(containers, project_name): uptodate_containers.append(container) return output + + +def get_hook_loaders_by_identifier(): + """Discovers pre/post hooks for loader plugins. + + Returns: + (dict) {"LoaderName": {"pre": ["PreLoader1"], "post":["PreLoader2]} + """ + # beware of circular imports! + from .plugins import PreLoadHookPlugin, PostLoadHookPlugin + hook_loaders_by_identifier = {} + _get_hook_loaders(hook_loaders_by_identifier, PreLoadHookPlugin, "pre") + _get_hook_loaders(hook_loaders_by_identifier, PostLoadHookPlugin, "post") + return hook_loaders_by_identifier + + +def _get_hook_loaders(hook_loaders_by_identifier, loader_plugin, loader_type): + load_hook_plugins = discover(loader_plugin) + loaders_by_name = get_loaders_by_name() + for hook_plugin in load_hook_plugins: + for load_plugin_name in hook_plugin.loaders: + load_plugin = loaders_by_name.get(load_plugin_name) + if not load_plugin: + continue + if not load_plugin.enabled: + continue + identifier = get_loader_identifier(load_plugin) + (hook_loaders_by_identifier.setdefault(identifier, {}) + .setdefault(loader_type, []).append( + hook_plugin) + ) diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index 7151dbdaec..71bd0c1289 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -18,7 +18,8 @@ from ayon_core.pipeline.load import ( load_with_product_contexts, LoadError, IncompatibleLoaderError, - get_loaders_by_name + get_loaders_by_name, + get_hook_loaders_by_identifier ) from ayon_core.tools.loader.abstract import ActionItem @@ -338,18 +339,7 @@ class LoaderActionsModel: available_loaders = self._filter_loaders_by_tool_name( project_name, discover_loader_plugins(project_name) ) - hook_loaders_by_identifier = {} - pre_load_hook_plugins = discover_loader_pre_hook_plugin(project_name) - loaders_by_name = get_loaders_by_name() - for hook_plugin in pre_load_hook_plugins: - for load_plugin_name in hook_plugin.loaders: - load_plugin = loaders_by_name.get(load_plugin_name) - if not load_plugin: - continue - if not load_plugin.enabled: - continue - identifier = get_loader_identifier(load_plugin) - hook_loaders_by_identifier.setdefault(identifier, {}).setdefault("pre", []).append(hook_plugin) + hook_loaders_by_identifier = get_hook_loaders_by_identifier() repre_loaders = [] product_loaders = [] loaders_by_identifier = {} From 969358d37a73abcbaf09df63137260b089a732df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 16:30:38 +0200 Subject: [PATCH 006/108] Removed missed unnecessary functions --- client/ayon_core/pipeline/__init__.py | 4 ---- client/ayon_core/tools/loader/models/actions.py | 1 - 2 files changed, 5 deletions(-) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index c6903f1b8e..beb5fa5ac2 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -42,13 +42,11 @@ from .load import ( register_loader_plugin_path, deregister_loader_plugin, - discover_loader_pre_hook_plugin, register_loader_pre_hook_plugin, deregister_loader_pre_hook_plugin, register_loader_pre_hook_plugin_path, deregister_loader_pre_hook_plugin_path, - discover_loader_post_hook_plugin, register_loader_post_hook_plugin, deregister_loader_post_hook_plugin, register_loader_post_hook_plugin_path, @@ -172,13 +170,11 @@ __all__ = ( "register_loader_plugin_path", "deregister_loader_plugin", - "discover_loader_pre_hook_plugin", "register_loader_pre_hook_plugin", "deregister_loader_pre_hook_plugin", "register_loader_pre_hook_plugin_path", "deregister_loader_pre_hook_plugin_path", - "discover_loader_post_hook_plugin", "register_loader_post_hook_plugin", "deregister_loader_post_hook_plugin", "register_loader_post_hook_plugin_path", diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index 71bd0c1289..07195f4b05 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -9,7 +9,6 @@ import ayon_api from ayon_core.lib import NestedCacheItem from ayon_core.pipeline.load import ( discover_loader_plugins, - discover_loader_pre_hook_plugin, ProductLoaderPlugin, filter_repre_contexts_by_loader, get_loader_identifier, From 42d869973c40ae8508d620fc904ce29ea5909e10 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 16:42:23 +0200 Subject: [PATCH 007/108] Fixed circular import --- client/ayon_core/pipeline/load/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 9d3635a186..910ffb2eac 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -16,7 +16,6 @@ from ayon_core.lib import ( ) from ayon_core.pipeline import ( Anatomy, - discover ) log = logging.getLogger(__name__) @@ -413,7 +412,7 @@ def load_with_product_contexts( ) -def _load_context(Loader, contexts, hooks, name, namespace, options): +def _load_context(Loader, contexts, name, namespace, options, hooks): """Helper function to wrap hooks around generic load function. Only dynamic part is different context(s) to be loaded. @@ -1160,6 +1159,7 @@ def get_hook_loaders_by_identifier(): """ # beware of circular imports! from .plugins import PreLoadHookPlugin, PostLoadHookPlugin + hook_loaders_by_identifier = {} _get_hook_loaders(hook_loaders_by_identifier, PreLoadHookPlugin, "pre") _get_hook_loaders(hook_loaders_by_identifier, PostLoadHookPlugin, "post") @@ -1167,6 +1167,8 @@ def get_hook_loaders_by_identifier(): def _get_hook_loaders(hook_loaders_by_identifier, loader_plugin, loader_type): + from ..plugin_discover import discover + load_hook_plugins = discover(loader_plugin) loaders_by_name = get_loaders_by_name() for hook_plugin in load_hook_plugins: From 3961f8a5e2282834d6852d49192527365d0e8b8c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 17:07:53 +0200 Subject: [PATCH 008/108] Added get_hook_loaders_by_identifier to pipeline API --- client/ayon_core/pipeline/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index beb5fa5ac2..0a805b16dc 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -61,6 +61,7 @@ from .load import ( get_representation_path, get_representation_context, get_repres_contexts, + get_hook_loaders_by_identifier ) from .publish import ( @@ -240,6 +241,8 @@ __all__ = ( "register_workfile_build_plugin_path", "deregister_workfile_build_plugin_path", + "get_hook_loaders_by_identifier", + # Backwards compatible function names "install", "uninstall", From a963f8c137896ec326695c01fdefa509cde9d510 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 17:08:13 +0200 Subject: [PATCH 009/108] Added get_hook_loaders_by_identifier SceneInventory controller --- client/ayon_core/tools/sceneinventory/control.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 60d9bc77a9..acbd67b14d 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -5,6 +5,7 @@ from ayon_core.host import HostBase from ayon_core.pipeline import ( registered_host, get_current_context, + get_hook_loaders_by_identifier ) from ayon_core.tools.common_models import HierarchyModel, ProjectsModel @@ -35,6 +36,8 @@ class SceneInventoryController: self._projects_model = ProjectsModel(self) self._event_system = self._create_event_system() + self._hooks_by_identifier = get_hook_loaders_by_identifier() + def get_host(self) -> HostBase: return self._host @@ -115,6 +118,10 @@ class SceneInventoryController: return self._containers_model.get_version_items( project_name, product_ids) + def get_hook_loaders_by_identifier(self): + """Returns lists of pre|post hooks per Loader identifier.""" + return self._hooks_by_identifier + # Site Sync methods def is_sitesync_enabled(self): return self._sitesync_model.is_sitesync_enabled() From 761ef4b4af343496c42a2236b373069210b0758f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 17:08:33 +0200 Subject: [PATCH 010/108] Added update methods for Loader Hooks --- client/ayon_core/pipeline/load/plugins.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 6b1591221a..af38cf4c45 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -277,6 +277,10 @@ class PreLoadHookPlugin: def process(self, context, name=None, namespace=None, options=None): pass + def update(self, container, context): + pass + + class PostLoadHookPlugin: """Plugin that should be run after any Loaders in 'loaders' @@ -288,6 +292,9 @@ class PostLoadHookPlugin: def process(self, context, name=None, namespace=None, options=None): pass + def update(self, container, context): + pass + def discover_loader_plugins(project_name=None): from ayon_core.lib import Logger From f44b81a7e58c645481b9653378ece6a72f09f154 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 17:09:06 +0200 Subject: [PATCH 011/108] Pass hook_loaders_by_id in SceneInventory view --- client/ayon_core/tools/sceneinventory/view.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index bb95e37d4e..75d3d9a680 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -1100,11 +1100,16 @@ class SceneInventoryView(QtWidgets.QTreeView): containers_by_id = self._controller.get_containers_by_item_ids( item_ids ) + hook_loaders_by_id = self._controller.get_hook_loaders_by_identifier() try: for item_id, item_version in zip(item_ids, versions): container = containers_by_id[item_id] try: - update_container(container, item_version) + update_container( + container, + item_version, + hook_loaders_by_id + ) except AssertionError: log.warning("Update failed", exc_info=True) self._show_version_error_dialog( From a5c10767fd3352aa2c7772d16b0e04d094b198fd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 17:09:32 +0200 Subject: [PATCH 012/108] Added loader hooks to update_container --- client/ayon_core/pipeline/load/utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 910ffb2eac..abef1bc500 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -520,7 +520,7 @@ def remove_container(container): return Loader().remove(container) -def update_container(container, version=-1): +def update_container(container, version=-1, hooks_by_identifier=None): """Update a container""" from ayon_core.pipeline import get_current_project_name @@ -616,7 +616,20 @@ def update_container(container, version=-1): if not path or not os.path.exists(path): raise ValueError("Path {} doesn't exist".format(path)) - return Loader().update(container, context) + loader_identifier = get_loader_identifier(Loader) + hooks = hooks_by_identifier.get(loader_identifier, {}) + for hook_plugin in hooks.get("pre", []): + hook_plugin.update( + context, + container + ) + update_return = Loader().update(container, context) + for hook_plugin in hooks.get("post", []): + hook_plugin.update( + context, + container + ) + return update_return def switch_container(container, representation, loader_plugin=None): From 82ae29dc12ce92ee748b9c19feab40dc80c4b877 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 17:10:47 +0200 Subject: [PATCH 013/108] Lazy initialization of get_hook_loaders_by_identifier --- client/ayon_core/tools/sceneinventory/control.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index acbd67b14d..e6fb459129 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -36,7 +36,7 @@ class SceneInventoryController: self._projects_model = ProjectsModel(self) self._event_system = self._create_event_system() - self._hooks_by_identifier = get_hook_loaders_by_identifier() + self._hooks_by_identifier = None def get_host(self) -> HostBase: return self._host @@ -120,6 +120,8 @@ class SceneInventoryController: def get_hook_loaders_by_identifier(self): """Returns lists of pre|post hooks per Loader identifier.""" + if self._hooks_by_identifier is None: + self._hooks_by_identifier = get_hook_loaders_by_identifier() return self._hooks_by_identifier # Site Sync methods From f609d2f5c44e2caf18b7f512e207b2eb6aeab856 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 17:19:24 +0200 Subject: [PATCH 014/108] Added switch methods to Loader Hooks --- client/ayon_core/pipeline/load/plugins.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index af38cf4c45..e930f034ce 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -280,6 +280,9 @@ class PreLoadHookPlugin: def update(self, container, context): pass + def switch(self, container, context): + pass + class PostLoadHookPlugin: """Plugin that should be run after any Loaders in 'loaders' @@ -295,6 +298,9 @@ class PostLoadHookPlugin: def update(self, container, context): pass + def switch(self, container, context): + pass + def discover_loader_plugins(project_name=None): from ayon_core.lib import Logger From 3a2d831ca632fdbfc93bf2789bdc7501424d5262 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Apr 2025 17:20:09 +0200 Subject: [PATCH 015/108] Added Loader Hooks to switch_container --- client/ayon_core/pipeline/load/utils.py | 27 ++++++++++++++++--- .../sceneinventory/switch_dialog/dialog.py | 8 +++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index abef1bc500..94e7ad53a6 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -632,15 +632,22 @@ def update_container(container, version=-1, hooks_by_identifier=None): return update_return -def switch_container(container, representation, loader_plugin=None): +def switch_container( + container, + representation, + loader_plugin=None, + hooks_by_identifier=None +): """Switch a container to representation Args: container (dict): container information representation (dict): representation entity + loader_plugin (LoaderPlugin) + hooks_by_identifier (dict): {"pre": [PreHookPlugin1], "post":[]} Returns: - function call + return from function call """ from ayon_core.pipeline import get_current_project_name @@ -679,7 +686,21 @@ def switch_container(container, representation, loader_plugin=None): loader = loader_plugin(context) - return loader.switch(container, context) + loader_identifier = get_loader_identifier(loader) + hooks = hooks_by_identifier.get(loader_identifier, {}) + for hook_plugin in hooks.get("pre", []): + hook_plugin.switch( + context, + container + ) + switch_return = loader.switch(container, context) + for hook_plugin in hooks.get("post", []): + hook_plugin.switch( + context, + container + ) + + return switch_return def _fix_representation_context_compatibility(repre_context): diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py index a6d88ed44a..c878cad079 100644 --- a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py +++ b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py @@ -1339,8 +1339,14 @@ class SwitchAssetDialog(QtWidgets.QDialog): repre_entity = repres_by_name[container_repre_name] error = None + hook_loaders_by_id = self._controller.get_hook_loaders_by_identifier() try: - switch_container(container, repre_entity, loader) + switch_container( + container, + repre_entity, + loader, + hook_loaders_by_id + ) except ( LoaderSwitchNotImplementedError, IncompatibleLoaderError, From 0070edb34b7ff68d79d3bb50593c478c1ab39774 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 7 Apr 2025 09:56:37 +0200 Subject: [PATCH 016/108] Updated docstrings --- client/ayon_core/tools/sceneinventory/control.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index e6fb459129..c4e59699c3 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -119,7 +119,11 @@ class SceneInventoryController: project_name, product_ids) def get_hook_loaders_by_identifier(self): - """Returns lists of pre|post hooks per Loader identifier.""" + """Returns lists of pre|post hooks per Loader identifier. + + Returns: + (dict) {"LoaderName": {"pre": ["PreLoader1"], "post":["PreLoader2]} + """ if self._hooks_by_identifier is None: self._hooks_by_identifier = get_hook_loaders_by_identifier() return self._hooks_by_identifier From 310174fd1a0fba4b0713475bc89e28385fe18e0e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 7 Apr 2025 10:18:19 +0200 Subject: [PATCH 017/108] Fix calling hook --- client/ayon_core/pipeline/load/utils.py | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 94e7ad53a6..d251dff06f 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -417,16 +417,16 @@ def _load_context(Loader, contexts, name, namespace, options, hooks): Only dynamic part is different context(s) to be loaded. """ - for hook_plugin in hooks.get("pre", []): - hook_plugin.process( + for hook_plugin_cls in hooks.get("pre", []): + hook_plugin_cls().process( contexts, name, namespace, options, ) load_return = Loader().load(contexts, name, namespace, options) - for hook_plugin in hooks.get("post", []): - hook_plugin.process( + for hook_plugin_cls in hooks.get("post", []): + hook_plugin_cls().process( contexts, name, namespace, @@ -618,14 +618,14 @@ def update_container(container, version=-1, hooks_by_identifier=None): loader_identifier = get_loader_identifier(Loader) hooks = hooks_by_identifier.get(loader_identifier, {}) - for hook_plugin in hooks.get("pre", []): - hook_plugin.update( + for hook_plugin_cls in hooks.get("pre", []): + hook_plugin_cls().update( context, container ) update_return = Loader().update(container, context) - for hook_plugin in hooks.get("post", []): - hook_plugin.update( + for hook_plugin_cls in hooks.get("post", []): + hook_plugin_cls().update( context, container ) @@ -688,14 +688,14 @@ def switch_container( loader_identifier = get_loader_identifier(loader) hooks = hooks_by_identifier.get(loader_identifier, {}) - for hook_plugin in hooks.get("pre", []): - hook_plugin.switch( + for hook_plugin_cls in hooks.get("pre", []): + hook_plugin_cls().switch( context, container ) switch_return = loader.switch(container, context) - for hook_plugin in hooks.get("post", []): - hook_plugin.switch( + for hook_plugin_cls in hooks.get("post", []): + hook_plugin_cls().switch( context, container ) @@ -1205,8 +1205,8 @@ def _get_hook_loaders(hook_loaders_by_identifier, loader_plugin, loader_type): load_hook_plugins = discover(loader_plugin) loaders_by_name = get_loaders_by_name() - for hook_plugin in load_hook_plugins: - for load_plugin_name in hook_plugin.loaders: + for hook_plugin_cls in load_hook_plugins: + for load_plugin_name in hook_plugin_cls.loaders: load_plugin = loaders_by_name.get(load_plugin_name) if not load_plugin: continue @@ -1215,5 +1215,5 @@ def _get_hook_loaders(hook_loaders_by_identifier, loader_plugin, loader_type): identifier = get_loader_identifier(load_plugin) (hook_loaders_by_identifier.setdefault(identifier, {}) .setdefault(loader_type, []).append( - hook_plugin) + hook_plugin_cls) ) From a0002774301c859b5b6906dc630500e117fed32f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 7 Apr 2025 10:57:58 +0200 Subject: [PATCH 018/108] Update naming Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index e930f034ce..9f9c6c223f 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -284,7 +284,7 @@ class PreLoadHookPlugin: pass -class PostLoadHookPlugin: +class PostLoaderHookPlugin: """Plugin that should be run after any Loaders in 'loaders' Should be used as non-invasive method to enrich core loading process. From d11a8d2731c2e22abf8da7c45103690b6b660535 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 7 Apr 2025 10:58:07 +0200 Subject: [PATCH 019/108] Update naming Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 9f9c6c223f..40a5eeb1a8 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -265,7 +265,7 @@ class ProductLoaderPlugin(LoaderPlugin): """ -class PreLoadHookPlugin: +class PreLoaderHookPlugin: """Plugin that should be run before any Loaders in 'loaders' Should be used as non-invasive method to enrich core loading process. From 70cf7fe5d41d282a23c8e974479ef32112d0081a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 7 Apr 2025 10:58:25 +0200 Subject: [PATCH 020/108] Import annotations Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 40a5eeb1a8..4ac1b3076d 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,3 +1,4 @@ +from __future__ import annotations import os import logging from typing import ClassVar From 44fc6f75ab1352cc8106543d612cbea64db0d03d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Apr 2025 10:40:52 +0200 Subject: [PATCH 021/108] Typo --- client/ayon_core/pipeline/load/plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index e930f034ce..4a503d6f1e 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -269,7 +269,7 @@ class PreLoadHookPlugin: """Plugin that should be run before any Loaders in 'loaders' Should be used as non-invasive method to enrich core loading process. - Any external studio might want to modify loaded data before or afte + Any external studio might want to modify loaded data before or after they are loaded without need to override existing core plugins. """ loaders: ClassVar[set[str]] @@ -288,7 +288,7 @@ class PostLoadHookPlugin: """Plugin that should be run after any Loaders in 'loaders' Should be used as non-invasive method to enrich core loading process. - Any external studio might want to modify loaded data before or afte + Any external studio might want to modify loaded data before or after they are loaded without need to override existing core plugins. loaders: ClassVar[set[str]] """ From 2fe9381471c619c2bff46ea0dfb50d4d5f8dc9ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Apr 2025 10:44:21 +0200 Subject: [PATCH 022/108] Added loaded container to Post process arguments --- client/ayon_core/pipeline/load/plugins.py | 9 ++++++++- client/ayon_core/pipeline/load/utils.py | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 4a503d6f1e..1719ee8c07 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -292,7 +292,14 @@ class PostLoadHookPlugin: they are loaded without need to override existing core plugins. loaders: ClassVar[set[str]] """ - def process(self, context, name=None, namespace=None, options=None): + def process( + self, + container, + context, + name=None, + namespace=None, + options=None + ): pass def update(self, container, context): diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index d251dff06f..471d93bb63 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -427,6 +427,7 @@ def _load_context(Loader, contexts, name, namespace, options, hooks): load_return = Loader().load(contexts, name, namespace, options) for hook_plugin_cls in hooks.get("post", []): hook_plugin_cls().process( + load_return, contexts, name, namespace, From fb1879a645e6f16f0bb9dd81349b3cd052b7d91c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Apr 2025 10:46:11 +0200 Subject: [PATCH 023/108] Updated names --- client/ayon_core/pipeline/load/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 471d93bb63..718316a4c2 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -424,16 +424,16 @@ def _load_context(Loader, contexts, name, namespace, options, hooks): namespace, options, ) - load_return = Loader().load(contexts, name, namespace, options) + loaded_container = Loader().load(contexts, name, namespace, options) for hook_plugin_cls in hooks.get("post", []): hook_plugin_cls().process( - load_return, + loaded_container, contexts, name, namespace, options, ) - return load_return + return loaded_container def load_container( @@ -624,13 +624,13 @@ def update_container(container, version=-1, hooks_by_identifier=None): context, container ) - update_return = Loader().update(container, context) + updated_container = Loader().update(container, context) for hook_plugin_cls in hooks.get("post", []): hook_plugin_cls().update( context, container ) - return update_return + return updated_container def switch_container( @@ -694,14 +694,14 @@ def switch_container( context, container ) - switch_return = loader.switch(container, context) + switched_container = loader.switch(container, context) for hook_plugin_cls in hooks.get("post", []): hook_plugin_cls().switch( context, container ) - return switch_return + return switched_container def _fix_representation_context_compatibility(repre_context): From 1e5f5446a77d5b0872a02498d31b4925a9c7cefa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Apr 2025 10:49:04 +0200 Subject: [PATCH 024/108] Renamed loaders to more descriptive --- client/ayon_core/pipeline/load/plugins.py | 5 +++-- client/ayon_core/pipeline/load/utils.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 1719ee8c07..26363a40d9 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -272,7 +272,7 @@ class PreLoadHookPlugin: Any external studio might want to modify loaded data before or after they are loaded without need to override existing core plugins. """ - loaders: ClassVar[set[str]] + loader_identifiers: ClassVar[set[str]] def process(self, context, name=None, namespace=None, options=None): pass @@ -290,8 +290,9 @@ class PostLoadHookPlugin: Should be used as non-invasive method to enrich core loading process. Any external studio might want to modify loaded data before or after they are loaded without need to override existing core plugins. - loaders: ClassVar[set[str]] """ + loader_identifiers: ClassVar[set[str]] + def process( self, container, diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 718316a4c2..c61d743d05 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -1207,7 +1207,7 @@ def _get_hook_loaders(hook_loaders_by_identifier, loader_plugin, loader_type): load_hook_plugins = discover(loader_plugin) loaders_by_name = get_loaders_by_name() for hook_plugin_cls in load_hook_plugins: - for load_plugin_name in hook_plugin_cls.loaders: + for load_plugin_name in hook_plugin_cls.loader_identifiers: load_plugin = loaders_by_name.get(load_plugin_name) if not load_plugin: continue From 8c90fac2b5568cd1e5f38417216d062eab1254bc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Apr 2025 12:36:21 +0200 Subject: [PATCH 025/108] Fix renamed class names --- client/ayon_core/pipeline/load/plugins.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 02efe5b43a..d069508bfa 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -350,32 +350,32 @@ def register_loader_plugin_path(path): def register_loader_pre_hook_plugin(plugin): - return register_plugin(PreLoadHookPlugin, plugin) + return register_plugin(PreLoaderHookPlugin, plugin) def deregister_loader_pre_hook_plugin(plugin): - deregister_plugin(PreLoadHookPlugin, plugin) + deregister_plugin(PreLoaderHookPlugin, plugin) def register_loader_pre_hook_plugin_path(path): - return register_plugin_path(PreLoadHookPlugin, path) + return register_plugin_path(PreLoaderHookPlugin, path) def deregister_loader_pre_hook_plugin_path(path): - deregister_plugin_path(PreLoadHookPlugin, path) + deregister_plugin_path(PreLoaderHookPlugin, path) def register_loader_post_hook_plugin(plugin): - return register_plugin(PostLoadHookPlugin, plugin) + return register_plugin(PostLoaderHookPlugin, plugin) def deregister_loader_post_hook_plugin(plugin): - deregister_plugin(PostLoadHookPlugin, plugin) + deregister_plugin(PostLoaderHookPlugin, plugin) def register_loader_post_hook_plugin_path(path): - return register_plugin_path(PostLoadHookPlugin, path) + return register_plugin_path(PostLoaderHookPlugin, path) def deregister_loader_post_hook_plugin_path(path): - deregister_plugin_path(PostLoadHookPlugin, path) + deregister_plugin_path(PostLoaderHookPlugin, path) From e86450c48df1bc78c400cfa750605192e1c48125 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 May 2025 12:57:56 +0200 Subject: [PATCH 026/108] Allow Templated Workfile Build to build from an AYON Entity URI instead of filepath or templated filepath. --- .../workfile/workfile_template_builder.py | 184 ++++++++++++------ 1 file changed, 121 insertions(+), 63 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 27da278c5e..319ebb954e 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -17,6 +17,7 @@ import collections import copy from abc import ABC, abstractmethod +import ayon_api from ayon_api import ( get_folders, get_folder_by_path, @@ -60,6 +61,28 @@ from ayon_core.pipeline.create import ( _NOT_SET = object() +def resolve_entity_uri(entity_uri: str) -> str: + """Resolve AYON entity URI to a filesystem path for local system.""" + response = ayon_api.post( + "resolve", + resolveRoots=True, + uris=[entity_uri] + ) + if response.status_code != 200: + raise RuntimeError( + f"Unable to resolve AYON entity URI filepath for " + f"'{entity_uri}': {response.text}" + ) + + entities = response.data[0]["entities"] + if len(entities) != 1: + raise RuntimeError( + f"Unable to resolve AYON entity URI '{entity_uri}' to a " + f"single filepath. Received data: {response.data}" + ) + return entities[0]["filePath"] + + class TemplateNotFound(Exception): """Exception raised when template does not exist.""" pass @@ -823,7 +846,6 @@ class AbstractTemplateBuilder(ABC): """ host_name = self.host_name - project_name = self.project_name task_name = self.current_task_name task_type = self.current_task_type @@ -836,6 +858,8 @@ class AbstractTemplateBuilder(ABC): } ) + print("Build profiles", build_profiles) + if not profile: raise TemplateProfileNotFound(( "No matching profile found for task '{}' of type '{}' " @@ -843,6 +867,15 @@ class AbstractTemplateBuilder(ABC): ).format(task_name, task_type, host_name)) path = profile["path"] + if not path: + raise TemplateLoadFailed(( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles" + ).format(host_name.title())) + + resolved_path = self.resolve_template_path(path) + self.log.info(f"Found template at: '{resolved_path}'") # switch to remove placeholders after they are used keep_placeholder = profile.get("keep_placeholder") @@ -852,86 +885,111 @@ class AbstractTemplateBuilder(ABC): if keep_placeholder is None: keep_placeholder = True - if not path: - raise TemplateLoadFailed(( - "Template path is not set.\n" - "Path need to be set in {}\\Template Workfile Build " - "Settings\\Profiles" - ).format(host_name.title())) - - # Try to fill path with environments and anatomy roots - anatomy = Anatomy(project_name) - fill_data = { - key: value - for key, value in os.environ.items() - } - - fill_data["root"] = anatomy.roots - fill_data["project"] = { - "name": project_name, - "code": anatomy.project_code, - } - - path = self.resolve_template_path(path, fill_data) - - if path and os.path.exists(path): - self.log.info("Found template at: '{}'".format(path)) - return { - "path": path, - "keep_placeholder": keep_placeholder, - "create_first_version": create_first_version - } - - solved_path = None - while True: - try: - solved_path = anatomy.path_remapper(path) - except KeyError as missing_key: - raise KeyError( - "Could not solve key '{}' in template path '{}'".format( - missing_key, path)) - - if solved_path is None: - solved_path = path - if solved_path == path: - break - path = solved_path - - solved_path = os.path.normpath(solved_path) - if not os.path.exists(solved_path): - raise TemplateNotFound( - "Template found in AYON settings for task '{}' with host " - "'{}' does not exists. (Not found : {})".format( - task_name, host_name, solved_path)) - - self.log.info("Found template at: '{}'".format(solved_path)) - return { - "path": solved_path, + "path": resolved_path, "keep_placeholder": keep_placeholder, "create_first_version": create_first_version } - def resolve_template_path(self, path, fill_data) -> str: + def resolve_template_path(self, path, fill_data=None) -> str: """Resolve the template path. - By default, this does nothing except returning the path directly. + By default, this: + - Resolves AYON entity URI to a filesystem path + - Returns path directly if it exists on disk. + - Resolves template keys through anatomy and environment variables. This can be overridden in host integrations to perform additional resolving over the template. Like, `hou.text.expandString` in Houdini. + It's recommended to still call the super().resolve_template_path() + to ensure the basic resolving is done across all integrations. Arguments: path (str): The input path. - fill_data (dict[str, str]): Data to use for template formatting. + fill_data (dict[str, str]): Deprecated. This is computed inside + the method using the current environment and project settings. + Used to be the data to use for template formatting. Returns: str: The resolved path. """ - result = StringTemplate.format_template(path, fill_data) - if result.solved: - path = result.normalized() - return path + + # If the path is an AYON entity URI, then resolve the filepath + # through the backend + if path.startswith("ayon+entity://") or path.startswith("ayon://"): + # This is a special case where the path is an AYON entity URI + # We need to resolve it to a filesystem path + resolved_path = resolve_entity_uri(path) + if not os.path.exists(resolved_path): + raise TemplateNotFound( + "Template found in AYON settings for task '{}' with host " + "'{}' does not resolve AYON entity URI '{}' " + "to an existing file on disk: '{}'".format( + self.current_task_name, + self.host_name, + path, + resolved_path, + ) + ) + return resolved_path + + # If the path is set and it's found on disk, return it directly + if path and os.path.exists(path): + return path + + # Otherwise assume a path with template keys, we do a very mundane + # check whether `{` or `<` is present in the path. + if "{" in path or "<" in path: + # Resolve keys through anatomy + project_name = self.project_name + task_name = self.current_task_name + host_name = self.host_name + + # Try to fill path with environments and anatomy roots + anatomy = Anatomy(project_name) + fill_data = { + key: value + for key, value in os.environ.items() + } + + fill_data["root"] = anatomy.roots + fill_data["project"] = { + "name": project_name, + "code": anatomy.project_code, + } + + # Recursively remap anatomy paths + while True: + try: + solved_path = anatomy.path_remapper(path) + except KeyError as missing_key: + raise KeyError( + f"Could not solve key '{missing_key}'" + f" in template path '{path}'" + ) + + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + + solved_path = os.path.normpath(solved_path) + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in AYON settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + result = StringTemplate.format_template(path, fill_data) + if result.solved: + path = result.normalized() + return path + + raise TemplateNotFound( + f"Unable to resolve template path: '{path}'" + ) def emit_event(self, topic, data=None, source=None) -> Event: return self._event_system.emit(topic, data, source) From 956121ba9649d1013ee36b7e1949c871e1795981 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 May 2025 13:01:43 +0200 Subject: [PATCH 027/108] Remove debug print --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 319ebb954e..f756190991 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -857,9 +857,6 @@ class AbstractTemplateBuilder(ABC): "task_names": task_name } ) - - print("Build profiles", build_profiles) - if not profile: raise TemplateProfileNotFound(( "No matching profile found for task '{}' of type '{}' " From 91df75f8eaaf0b6a74113c1d4facfd9be597d90f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 30 May 2025 16:10:20 +0200 Subject: [PATCH 028/108] :sparkles: add product base type support to loaders --- client/ayon_core/pipeline/load/plugins.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 4a11b929cc..7a92ed943d 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,3 +1,5 @@ +"""Plugins for loading representations and products into host applications.""" +from __future__ import annotations import os import logging @@ -15,7 +17,8 @@ from .utils import get_representation_path_from_context class LoaderPlugin(list): """Load representation into host application""" - product_types = set() + product_types: set[str] = set() + product_base_types: set[str] = set() representations = set() extensions = {"*"} order = 0 @@ -122,9 +125,11 @@ class LoaderPlugin(list): plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types + plugin_product_base_types = cls.product_base_types if ( not plugin_repre_names or not plugin_product_types + or not plugin_product_base_types or not cls.extensions ): return False @@ -147,10 +152,20 @@ class LoaderPlugin(list): if "*" in plugin_product_types: return True + plugin_product_base_types = set(plugin_product_base_types) + if "*" in plugin_product_base_types: + # If plugin supports all product base types, then it is compatible + # with any product type. + return True + product_entity = context["product"] product_type = product_entity["productType"] + product_base_type = product_entity.get("productBaseType") - return product_type in plugin_product_types + if product_type in plugin_product_types: + return True + + return product_base_type in plugin_product_base_types @classmethod def get_representations(cls): From fac933c16ab73d7f893e27ab6f0a022e2dd5dcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 30 May 2025 16:46:18 +0200 Subject: [PATCH 029/108] :recycle: make the check backwards compatible Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 7a92ed943d..32b96e3e7a 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -128,8 +128,7 @@ class LoaderPlugin(list): plugin_product_base_types = cls.product_base_types if ( not plugin_repre_names - or not plugin_product_types - or not plugin_product_base_types + or (not plugin_product_types and not plugin_product_base_types) or not cls.extensions ): return False From 6ea717bc3624cd17da53dd676772278704ac87d3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 6 Jun 2025 10:01:32 +0200 Subject: [PATCH 030/108] :wrench: WIP on product base type support in loader tool --- client/ayon_core/tools/loader/abstract.py | 140 +++++++++++++----- .../ayon_core/tools/loader/models/products.py | 132 +++++++++++++++-- 2 files changed, 226 insertions(+), 46 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index d0d7cd430b..741eb59f81 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,5 +1,6 @@ +from __future__ import annotations from abc import ABC, abstractmethod -from typing import List +from typing import List, Optional, TypedDict from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -8,15 +9,62 @@ from ayon_core.lib.attribute_definitions import ( ) +IconData = TypedDict("IconData", { + "type": str, + "name": str, + "color": str +}) + +ProductBaseTypeItemData = TypedDict("ProductBaseTypeItemData", { + "name": str, + "icon": IconData +}) + + +VersionItemData = TypedDict("VersionItemData", { + "version_id": str, + "version": int, + "is_hero": bool, + "product_id": str, + "task_id": Optional[str], + "thumbnail_id": Optional[str], + "published_time": Optional[str], + "author": Optional[str], + "status": Optional[str], + "frame_range": Optional[str], + "duration": Optional[int], + "handles": Optional[str], + "step": Optional[int], + "comment": Optional[str], + "source": Optional[str] +}) + + +ProductItemData = TypedDict("ProductItemData", { + "product_id": str, + "product_type": str, + "product_base_type": str, + "product_name": str, + "product_icon": IconData, + "product_type_icon": IconData, + "product_base_type_icon": IconData, + "group_name": str, + "folder_id": str, + "folder_label": str, + "version_items": dict[str, VersionItemData], + "product_in_scene": bool +}) + + class ProductTypeItem: """Item representing product type. Args: name (str): Product type name. - icon (dict[str, Any]): Product type icon definition. + icon (IconData): Product type icon definition. """ - def __init__(self, name, icon): + def __init__(self, name: str, icon: IconData): self.name = name self.icon = icon @@ -31,6 +79,24 @@ class ProductTypeItem: return cls(**data) +class ProductBaseTypeItem: + """Item representing product base type.""" + + def __init__(self, name: str, icon: IconData): + self.name = name + self.icon = icon + + def to_data(self) -> ProductBaseTypeItemData: + return { + "name": self.name, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data: ProductBaseTypeItemData): + return cls(**data) + + class ProductItem: """Product item with it versions. @@ -38,8 +104,8 @@ class ProductItem: product_id (str): Product id. product_type (str): Product type. product_name (str): Product name. - product_icon (dict[str, Any]): Product icon definition. - product_type_icon (dict[str, Any]): Product type icon definition. + product_icon (IconData): Product icon definition. + product_type_icon (IconData): Product type icon definition. product_in_scene (bool): Is product in scene (only when used in DCC). group_name (str): Group name. folder_id (str): Folder id. @@ -49,35 +115,42 @@ class ProductItem: def __init__( self, - product_id, - product_type, - product_name, - product_icon, - product_type_icon, - product_in_scene, - group_name, - folder_id, - folder_label, - version_items, + product_id: str, + product_type: str, + product_base_type: str, + product_name: str, + product_icon: IconData, + product_type_icon: IconData, + product_base_type_icon: IconData, + group_name: str, + folder_id: str, + folder_label: str, + version_items: dict[str, VersionItem], + *, + product_in_scene: bool, ): self.product_id = product_id self.product_type = product_type + self.product_base_type = product_base_type self.product_name = product_name self.product_icon = product_icon self.product_type_icon = product_type_icon + self.product_base_type_icon = product_base_type_icon self.product_in_scene = product_in_scene self.group_name = group_name self.folder_id = folder_id self.folder_label = folder_label self.version_items = version_items - def to_data(self): + def to_data(self) -> ProductItemData: return { "product_id": self.product_id, "product_type": self.product_type, + "product_base_type": self.product_base_type, "product_name": self.product_name, "product_icon": self.product_icon, "product_type_icon": self.product_type_icon, + "product_base_type_icon": self.product_base_type_icon, "product_in_scene": self.product_in_scene, "group_name": self.group_name, "folder_id": self.folder_id, @@ -124,21 +197,22 @@ class VersionItem: def __init__( self, - version_id, - version, - is_hero, - product_id, - task_id, - thumbnail_id, - published_time, - author, - status, - frame_range, - duration, - handles, - step, - comment, - source, + *, + version_id: str, + version: int, + is_hero: bool, + product_id: str, + task_id: Optional[str] = None, + thumbnail_id: Optional[str] = None, + published_time: Optional[str] = None, + author: Optional[str] = None, + status: Optional[str] = None, + frame_range: Optional[str] = None, + duration: Optional[int] = None, + handles: Optional[str] = None, + step: Optional[int] = None, + comment: Optional[str] = None, + source: Optional[str] = None, ): self.version_id = version_id self.product_id = product_id @@ -198,7 +272,7 @@ class VersionItem: def __le__(self, other): return self.__eq__(other) or self.__lt__(other) - def to_data(self): + def to_data(self) -> VersionItemData: return { "version_id": self.version_id, "product_id": self.product_id, @@ -218,7 +292,7 @@ class VersionItem: } @classmethod - def from_data(cls, data): + def from_data(cls, data: VersionItemData): return cls(**data) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 34acc0550c..da2b049f50 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -1,19 +1,29 @@ +"""Products model for loader tools.""" +from __future__ import annotations import collections import contextlib +from typing import TYPE_CHECKING, Iterable, Optional import arrow import ayon_api from ayon_api.operations import OperationsSession + from ayon_core.lib import NestedCacheItem from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.loader.abstract import ( + IconData, ProductTypeItem, + ProductBaseTypeItem, ProductItem, VersionItem, RepreItem, ) +if TYPE_CHECKING: + from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict + + PRODUCTS_MODEL_SENDER = "products.model" @@ -70,9 +80,10 @@ def version_item_from_entity(version): def product_item_from_entity( - product_entity, + product_entity: ProductDict, version_entities, - product_type_items_by_name, + product_type_items_by_name: dict[str, ProductTypeItem], + product_base_type_items_by_name: dict[str, ProductBaseTypeItem], folder_label, product_in_scene, ): @@ -88,9 +99,21 @@ def product_item_from_entity( # Cache the item for future use product_type_items_by_name[product_type] = product_type_item - product_type_icon = product_type_item.icon + product_base_type = product_entity.get("productBaseType") + product_base_type_item = product_base_type_items_by_name.get( + product_base_type) + # Same as for product type item above. Not sure if this is still needed + # though. + if product_base_type_item is None: + product_base_type_item = create_default_product_base_type_item( + product_base_type) + # Cache the item for future use + product_base_type_items_by_name[product_base_type] = ( + product_base_type_item) - product_icon = { + product_type_icon = product_type_item.icon + product_base_type_icon = product_base_type_item.icon + product_icon: IconData = { "type": "awesome-font", "name": "fa.file-o", "color": get_default_entity_icon_color(), @@ -103,9 +126,11 @@ def product_item_from_entity( return ProductItem( product_id=product_entity["id"], product_type=product_type, + product_base_type=product_base_type, product_name=product_entity["name"], product_icon=product_icon, product_type_icon=product_type_icon, + product_base_type_icon=product_base_type_icon, product_in_scene=product_in_scene, group_name=group, folder_id=product_entity["folderId"], @@ -114,11 +139,12 @@ def product_item_from_entity( ) -def product_type_item_from_data(product_type_data): +def product_type_item_from_data( + product_type_data: ProductDict) -> ProductTypeItem: # TODO implement icon implementation # icon = product_type_data["icon"] # color = product_type_data["color"] - icon = { + icon: IconData = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -127,8 +153,30 @@ def product_type_item_from_data(product_type_data): return ProductTypeItem(product_type_data["name"], icon) -def create_default_product_type_item(product_type): - icon = { +def product_base_type_item_from_data( + product_base_type_data: ProductBaseTypeDict +) -> ProductBaseTypeItem: + """Create product base type item from data. + + Args: + product_base_type_data (ProductBaseTypeDict): Product base type data. + + Returns: + ProductBaseTypeDict: Product base type item. + + """ + icon: IconData = { + "type": "awesome-font", + "name": "fa.folder", + "color": "#0091B2", + } + return ProductBaseTypeItem( + name=product_base_type_data["name"], + icon=icon) + + +def create_default_product_type_item(product_type: str) -> ProductTypeItem: + icon: IconData = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -136,10 +184,28 @@ def create_default_product_type_item(product_type): return ProductTypeItem(product_type, icon) +def create_default_product_base_type_item( + product_base_type: str) -> ProductBaseTypeItem: + """Create default product base type item. + + Args: + product_base_type (str): Product base type name. + + Returns: + ProductBaseTypeItem: Default product base type item. + """ + icon: IconData = { + "type": "awesome-font", + "name": "fa.folder", + "color": "#0091B2", + } + return ProductBaseTypeItem(product_base_type, icon) + + class ProductsModel: """Model for products, version and representation. - All of the entities are product based. This model prepares data for UI + All the entities are product based. This model prepares data for UI and caches it for faster access. Note: @@ -161,6 +227,8 @@ class ProductsModel: # Cache helpers self._product_type_items_cache = NestedCacheItem( levels=1, default_factory=list, lifetime=self.lifetime) + self._product_base_type_items_cache = NestedCacheItem( + levels=1, default_factory=list, lifetime=self.lifetime) self._product_items_cache = NestedCacheItem( levels=2, default_factory=dict, lifetime=self.lifetime) self._repre_items_cache = NestedCacheItem( @@ -199,6 +267,31 @@ class ProductsModel: ]) return cache.get_data() + def get_product_base_type_items( + self, + project_name: Optional[str]) -> list[ProductBaseTypeItem]: + """Product base type items for project. + + Args: + project_name (optional, str): Project name. + + Returns: + list[ProductBaseTypeDict]: Product base type items. + + """ + if not project_name: + return [] + + cache = self._product_base_type_items_cache[project_name] + if not cache.is_valid: + product_base_types = ayon_api.get_project_product_base_types( + project_name) + cache.update_data([ + product_base_type_item_from_data(product_base_type) + for product_base_type in product_base_types + ]) + return cache.get_data() + def get_product_items(self, project_name, folder_ids, sender): """Product items with versions for project and folder ids. @@ -449,11 +542,12 @@ class ProductsModel: def _create_product_items( self, - project_name, - products, - versions, + project_name: str, + products: Iterable[ProductDict], + versions: Iterable[VersionDict], folder_items=None, product_type_items=None, + product_base_type_items: Optional[Iterable[ProductBaseTypeItem]] = None ): if folder_items is None: folder_items = self._controller.get_folder_items(project_name) @@ -461,6 +555,11 @@ class ProductsModel: if product_type_items is None: product_type_items = self.get_product_type_items(project_name) + if product_base_type_items is None: + product_base_type_items = self.get_product_base_type_items( + project_name + ) + loaded_product_ids = self._controller.get_loaded_product_ids() versions_by_product_id = collections.defaultdict(list) @@ -470,7 +569,13 @@ class ProductsModel: product_type_item.name: product_type_item for product_type_item in product_type_items } - output = {} + + product_base_type_items_by_name: dict[str, ProductBaseTypeItem] = { + product_base_type_item.name: product_base_type_item + for product_base_type_item in product_base_type_items + } + + output: dict[str, ProductItem] = {} for product in products: product_id = product["id"] folder_id = product["folderId"] @@ -484,6 +589,7 @@ class ProductsModel: product, versions, product_type_items_by_name, + product_base_type_items_by_name, folder_item.label, product_id in loaded_product_ids, ) From 3a2f470dce3690c335466ecc01d1ff14588753be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 14:03:31 +0200 Subject: [PATCH 031/108] :sparkles: show product base type in the loader --- client/ayon_core/tools/loader/abstract.py | 26 +++++++-- .../ayon_core/tools/loader/models/products.py | 2 +- .../tools/loader/ui/products_model.py | 55 +++++++++++-------- .../tools/loader/ui/products_widget.py | 1 + 4 files changed, 55 insertions(+), 29 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 741eb59f81..d6a4bf40cb 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,14 +1,15 @@ +"""Abstract base classes for loader tool.""" from __future__ import annotations + from abc import ABC, abstractmethod from typing import List, Optional, TypedDict from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, - serialize_attr_defs, deserialize_attr_defs, + serialize_attr_defs, ) - IconData = TypedDict("IconData", { "type": str, "name": str, @@ -80,20 +81,37 @@ class ProductTypeItem: class ProductBaseTypeItem: - """Item representing product base type.""" + """Item representing the product base type.""" def __init__(self, name: str, icon: IconData): + """Initialize product base type item.""" self.name = name self.icon = icon def to_data(self) -> ProductBaseTypeItemData: + """Convert item to data dictionary. + + Returns: + ProductBaseTypeItemData: Data representation of the item. + + """ return { "name": self.name, "icon": self.icon, } @classmethod - def from_data(cls, data: ProductBaseTypeItemData): + def from_data( + cls, data: ProductBaseTypeItemData) -> ProductBaseTypeItem: + """Create item from data dictionary. + + Args: + data (ProductBaseTypeItemData): Data to create item from. + + Returns: + ProductBaseTypeItem: Item created from the provided data. + + """ return cls(**data) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index da2b049f50..41919461d0 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -270,7 +270,7 @@ class ProductsModel: def get_product_base_type_items( self, project_name: Optional[str]) -> list[ProductBaseTypeItem]: - """Product base type items for project. + """Product base type items for the project. Args: project_name (optional, str): Project name. diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index cebae9bca7..24050fc0c1 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -16,31 +16,32 @@ TASK_ID_ROLE = QtCore.Qt.UserRole + 5 PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6 PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7 PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8 -PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 9 -PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 10 -VERSION_ID_ROLE = QtCore.Qt.UserRole + 11 -VERSION_HERO_ROLE = QtCore.Qt.UserRole + 12 -VERSION_NAME_ROLE = QtCore.Qt.UserRole + 13 -VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 14 -VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 15 -VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 16 -VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 17 -VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 18 -VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 19 -VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 20 -VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 21 -VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 22 -VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 23 -VERSION_STEP_ROLE = QtCore.Qt.UserRole + 24 -VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 25 -VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 26 -ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 27 -REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28 -REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 29 -SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 30 -SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31 +PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9 +PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 10 +PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 11 +VERSION_ID_ROLE = QtCore.Qt.UserRole + 12 +VERSION_HERO_ROLE = QtCore.Qt.UserRole + 13 +VERSION_NAME_ROLE = QtCore.Qt.UserRole + 14 +VERSION_NAME_EDIT_ROLE = QtCore.Qt.UserRole + 15 +VERSION_PUBLISH_TIME_ROLE = QtCore.Qt.UserRole + 16 +VERSION_STATUS_NAME_ROLE = QtCore.Qt.UserRole + 17 +VERSION_STATUS_SHORT_ROLE = QtCore.Qt.UserRole + 18 +VERSION_STATUS_COLOR_ROLE = QtCore.Qt.UserRole + 19 +VERSION_STATUS_ICON_ROLE = QtCore.Qt.UserRole + 20 +VERSION_AUTHOR_ROLE = QtCore.Qt.UserRole + 21 +VERSION_FRAME_RANGE_ROLE = QtCore.Qt.UserRole + 22 +VERSION_DURATION_ROLE = QtCore.Qt.UserRole + 23 +VERSION_HANDLES_ROLE = QtCore.Qt.UserRole + 24 +VERSION_STEP_ROLE = QtCore.Qt.UserRole + 25 +VERSION_AVAILABLE_ROLE = QtCore.Qt.UserRole + 26 +VERSION_THUMBNAIL_ID_ROLE = QtCore.Qt.UserRole + 27 +ACTIVE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 28 +REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 29 +REPRESENTATIONS_COUNT_ROLE = QtCore.Qt.UserRole + 30 +SYNC_ACTIVE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 31 +SYNC_REMOTE_SITE_AVAILABILITY = QtCore.Qt.UserRole + 32 -STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 32 +STATUS_NAME_FILTER_ROLE = QtCore.Qt.UserRole + 33 class ProductsModel(QtGui.QStandardItemModel): @@ -49,6 +50,7 @@ class ProductsModel(QtGui.QStandardItemModel): column_labels = [ "Product name", "Product type", + "Product base type", "Folder", "Version", "Status", @@ -79,6 +81,7 @@ class ProductsModel(QtGui.QStandardItemModel): product_name_col = column_labels.index("Product name") product_type_col = column_labels.index("Product type") + product_base_type_col = column_labels.index("Product base type") folders_label_col = column_labels.index("Folder") version_col = column_labels.index("Version") status_col = column_labels.index("Status") @@ -93,6 +96,7 @@ class ProductsModel(QtGui.QStandardItemModel): _display_role_mapping = { product_name_col: QtCore.Qt.DisplayRole, product_type_col: PRODUCT_TYPE_ROLE, + product_base_type_col: PRODUCT_BASE_TYPE_ROLE, folders_label_col: FOLDER_LABEL_ROLE, version_col: VERSION_NAME_ROLE, status_col: VERSION_STATUS_NAME_ROLE, @@ -432,6 +436,9 @@ class ProductsModel(QtGui.QStandardItemModel): model_item.setData(icon, QtCore.Qt.DecorationRole) model_item.setData(product_id, PRODUCT_ID_ROLE) model_item.setData(product_item.product_name, PRODUCT_NAME_ROLE) + model_item.setData( + product_item.product_base_type, PRODUCT_BASE_TYPE_ROLE + ) model_item.setData(product_item.product_type, PRODUCT_TYPE_ROLE) model_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE) model_item.setData(product_item.folder_id, FOLDER_ID_ROLE) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 94d95b9026..67116ad544 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -142,6 +142,7 @@ class ProductsWidget(QtWidgets.QWidget): default_widths = ( 200, # Product name 90, # Product type + 90, # Product base type 130, # Folder label 60, # Version 100, # Status From a3357a3ace0039a171c510a1e6a30ab19a231bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 14:22:02 +0200 Subject: [PATCH 032/108] :dog: fix some linting issue --- client/ayon_core/tools/loader/ui/products_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index 24050fc0c1..8b8d4a67bf 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -16,7 +16,7 @@ TASK_ID_ROLE = QtCore.Qt.UserRole + 5 PRODUCT_ID_ROLE = QtCore.Qt.UserRole + 6 PRODUCT_NAME_ROLE = QtCore.Qt.UserRole + 7 PRODUCT_TYPE_ROLE = QtCore.Qt.UserRole + 8 -PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9 +PRODUCT_BASE_TYPE_ROLE = QtCore.Qt.UserRole + 9 PRODUCT_TYPE_ICON_ROLE = QtCore.Qt.UserRole + 10 PRODUCT_IN_SCENE_ROLE = QtCore.Qt.UserRole + 11 VERSION_ID_ROLE = QtCore.Qt.UserRole + 12 From 98eb281adc2533548fa807d23862e93e675299a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 14:22:25 +0200 Subject: [PATCH 033/108] :recycle: hide product base types if support is disabled --- client/ayon_core/tools/loader/ui/products_widget.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 67116ad544..511c346cb9 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -4,6 +4,7 @@ from typing import Optional from qtpy import QtWidgets, QtCore +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from ayon_core.tools.utils import ( RecursiveSortFilterProxyModel, DeselectableTreeView, @@ -262,6 +263,12 @@ class ProductsWidget(QtWidgets.QWidget): self._controller.is_sitesync_enabled() ) + if not is_supporting_product_base_type(): + # Hide product base type column + products_view.setColumnHidden( + products_model.product_base_type_col, True + ) + def set_name_filter(self, name): """Set filter of product name. From 74ed8bf2bb6b84499c48b395ab6ff70dacf1f2e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 9 Jun 2025 13:52:13 +0200 Subject: [PATCH 034/108] :recycle: refactor support check function name --- client/ayon_core/tools/loader/ui/products_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/ui/products_widget.py b/client/ayon_core/tools/loader/ui/products_widget.py index 511c346cb9..47046d5ec2 100644 --- a/client/ayon_core/tools/loader/ui/products_widget.py +++ b/client/ayon_core/tools/loader/ui/products_widget.py @@ -4,7 +4,7 @@ from typing import Optional from qtpy import QtWidgets, QtCore -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.tools.utils import ( RecursiveSortFilterProxyModel, DeselectableTreeView, @@ -263,7 +263,7 @@ class ProductsWidget(QtWidgets.QWidget): self._controller.is_sitesync_enabled() ) - if not is_supporting_product_base_type(): + if not is_product_base_type_supported(): # Hide product base type column products_view.setColumnHidden( products_model.product_base_type_col, True From aec2ee828cdd3686d476b53d761f3cd152b39133 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:23:02 +0200 Subject: [PATCH 035/108] Raise dedicated `EntityResolutionError` --- .../pipeline/workfile/workfile_template_builder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index f756190991..152421f283 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -61,6 +61,10 @@ from ayon_core.pipeline.create import ( _NOT_SET = object() +class EntityResolutionError(Exception): + """Exception raised when entity URI resolution fails.""" + + def resolve_entity_uri(entity_uri: str) -> str: """Resolve AYON entity URI to a filesystem path for local system.""" response = ayon_api.post( @@ -76,7 +80,7 @@ def resolve_entity_uri(entity_uri: str) -> str: entities = response.data[0]["entities"] if len(entities) != 1: - raise RuntimeError( + raise EntityResolutionError( f"Unable to resolve AYON entity URI '{entity_uri}' to a " f"single filepath. Received data: {response.data}" ) From 7b5ff1394c1684a9a1740ece36e5d343adf853b9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:30:48 +0200 Subject: [PATCH 036/108] Do not raise error on non-existing path from `resolve_template_path` to match backwards compatible behavior - feedback by @iLLiCiTiT --- .../workfile/workfile_template_builder.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 152421f283..74dc503cac 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -876,6 +876,12 @@ class AbstractTemplateBuilder(ABC): ).format(host_name.title())) resolved_path = self.resolve_template_path(path) + if not resolved_path: + raise TemplateNotFound( + f"Template path '{path}' does not resolve to a valid existing " + "template file on disk." + ) + self.log.info(f"Found template at: '{resolved_path}'") # switch to remove placeholders after they are used @@ -923,7 +929,7 @@ class AbstractTemplateBuilder(ABC): # We need to resolve it to a filesystem path resolved_path = resolve_entity_uri(path) if not os.path.exists(resolved_path): - raise TemplateNotFound( + self.log.warning( "Template found in AYON settings for task '{}' with host " "'{}' does not resolve AYON entity URI '{}' " "to an existing file on disk: '{}'".format( @@ -978,19 +984,22 @@ class AbstractTemplateBuilder(ABC): solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): - raise TemplateNotFound( + self.log.warning( "Template found in AYON settings for task '{}' with host " "'{}' does not exists. (Not found : {})".format( - task_name, host_name, solved_path)) + task_name, host_name, solved_path) + ) + return path result = StringTemplate.format_template(path, fill_data) if result.solved: path = result.normalized() return path - raise TemplateNotFound( + self.log.warning( f"Unable to resolve template path: '{path}'" ) + return path def emit_event(self, topic, data=None, source=None) -> Event: return self._event_system.emit(topic, data, source) From 706d72f045e179e9c633a4015719d8346a722ba0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:33:36 +0200 Subject: [PATCH 037/108] Check file existence for error check --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 74dc503cac..193fbeca2a 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -876,7 +876,7 @@ class AbstractTemplateBuilder(ABC): ).format(host_name.title())) resolved_path = self.resolve_template_path(path) - if not resolved_path: + if not resolved_path or not os.path.exists(resolved_path): raise TemplateNotFound( f"Template path '{path}' does not resolve to a valid existing " "template file on disk." From b523aa6b9f8286068268a88314377faca628d235 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:36:37 +0200 Subject: [PATCH 038/108] Move warning out of `resolve_template_path` function so that inherited function,e.g. like in ayon-houdini can continue without having tons of logs that are irrelevant if houdini specific logic could still resolve it. --- .../workfile/workfile_template_builder.py | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 193fbeca2a..ef909c9503 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -878,8 +878,9 @@ class AbstractTemplateBuilder(ABC): resolved_path = self.resolve_template_path(path) if not resolved_path or not os.path.exists(resolved_path): raise TemplateNotFound( - f"Template path '{path}' does not resolve to a valid existing " - "template file on disk." + "Template file found in AYON settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, resolved_path) ) self.log.info(f"Found template at: '{resolved_path}'") @@ -928,17 +929,6 @@ class AbstractTemplateBuilder(ABC): # This is a special case where the path is an AYON entity URI # We need to resolve it to a filesystem path resolved_path = resolve_entity_uri(path) - if not os.path.exists(resolved_path): - self.log.warning( - "Template found in AYON settings for task '{}' with host " - "'{}' does not resolve AYON entity URI '{}' " - "to an existing file on disk: '{}'".format( - self.current_task_name, - self.host_name, - path, - resolved_path, - ) - ) return resolved_path # If the path is set and it's found on disk, return it directly @@ -984,11 +974,6 @@ class AbstractTemplateBuilder(ABC): solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): - self.log.warning( - "Template found in AYON settings for task '{}' with host " - "'{}' does not exists. (Not found : {})".format( - task_name, host_name, solved_path) - ) return path result = StringTemplate.format_template(path, fill_data) @@ -996,9 +981,6 @@ class AbstractTemplateBuilder(ABC): path = result.normalized() return path - self.log.warning( - f"Unable to resolve template path: '{path}'" - ) return path def emit_event(self, topic, data=None, source=None) -> Event: From 16b56f7579b0b22f57db27e723f5d38761ee8404 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:37:16 +0200 Subject: [PATCH 039/108] Remove unused variables --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index ef909c9503..de2eb9ce5c 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -940,8 +940,6 @@ class AbstractTemplateBuilder(ABC): if "{" in path or "<" in path: # Resolve keys through anatomy project_name = self.project_name - task_name = self.current_task_name - host_name = self.host_name # Try to fill path with environments and anatomy roots anatomy = Anatomy(project_name) From 40d5efc9a5bf2e1ce14ff07b20fcf1b3b2f42ecf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 20:08:29 +0200 Subject: [PATCH 040/108] Fix typo --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index de2eb9ce5c..d62bb49227 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -8,7 +8,7 @@ targeted by task types and names. Placeholders are created using placeholder plugins which should care about logic and data of placeholder items. 'PlaceholderItem' is used to keep track -about it's progress. +about its progress. """ import os From d681c87aadbc14404511fd39996d9ac5ce2a3c06 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jun 2025 13:49:46 +0200 Subject: [PATCH 041/108] Restructure `resolve_template_path` --- .../workfile/workfile_template_builder.py | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index d62bb49227..8cea7de86b 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -935,51 +935,53 @@ class AbstractTemplateBuilder(ABC): if path and os.path.exists(path): return path - # Otherwise assume a path with template keys, we do a very mundane - # check whether `{` or `<` is present in the path. - if "{" in path or "<" in path: - # Resolve keys through anatomy - project_name = self.project_name + # We may have path for another platform, like C:/path/to/file + # or a path with template keys, like {project[code]} or both. + # Try to fill path with environments and anatomy roots + project_name = self.project_name + anatomy = Anatomy(project_name) - # Try to fill path with environments and anatomy roots - anatomy = Anatomy(project_name) + # Simple check whether the path contains any template keys + if "{" in path: fill_data = { key: value for key, value in os.environ.items() } - fill_data["root"] = anatomy.roots fill_data["project"] = { "name": project_name, "code": anatomy.project_code, } - # Recursively remap anatomy paths - while True: - try: - solved_path = anatomy.path_remapper(path) - except KeyError as missing_key: - raise KeyError( - f"Could not solve key '{missing_key}'" - f" in template path '{path}'" - ) - - if solved_path is None: - solved_path = path - if solved_path == path: - break - path = solved_path - - solved_path = os.path.normpath(solved_path) - if not os.path.exists(solved_path): + # Format the template using local fill data + result = StringTemplate.format_template(path, fill_data) + if not result.solved: return path - result = StringTemplate.format_template(path, fill_data) - if result.solved: - path = result.normalized() - return path + path = result.normalized() + if os.path.exists(path): + return path - return path + # If the path were set in settings using a Windows path and we + # are now on a Linux system, we try to convert the solved path to + # the current platform. + while True: + try: + solved_path = anatomy.path_remapper(path) + except KeyError as missing_key: + raise KeyError( + f"Could not solve key '{missing_key}'" + f" in template path '{path}'" + ) + + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + + solved_path = os.path.normpath(solved_path) + return solved_path def emit_event(self, topic, data=None, source=None) -> Event: return self._event_system.emit(topic, data, source) From 6e095b8c188b09e3f067538b40652aeaa1089821 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 14:35:23 +0200 Subject: [PATCH 042/108] Updated docstring --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 62d376c6d1..ed26e5fe74 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -257,7 +257,7 @@ class PreLoaderHookPlugin: """Plugin that should be run before any Loaders in 'loaders' Should be used as non-invasive method to enrich core loading process. - Any external studio might want to modify loaded data before or after + Any studio might want to modify loaded data before or after they are loaded without need to override existing core plugins. """ loader_identifiers: ClassVar[set[str]] From 0ac277404c953e26770e3a1aaf6d645ba85af2ed Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 14:37:03 +0200 Subject: [PATCH 043/108] Updated docstring --- client/ayon_core/tools/loader/models/actions.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index 07195f4b05..c2444da456 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -316,10 +316,8 @@ class LoaderActionsModel: we want to show loaders for? Returns: - tuple( - list[ProductLoaderPlugin], - list[LoaderPlugin], - ): Discovered loader plugins. + tuple[list[ProductLoaderPlugin], list[LoaderPlugin]]: Discovered + loader plugins. """ loaders_by_identifier_c = self._loaders_by_identifier[project_name] From ee96cdc2c38f1e04b9ba2ccffe4b18ecbe83c6e8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 14:44:14 +0200 Subject: [PATCH 044/108] Remove methods for pre/post loader hooks from higher api This "hides" a bit methods that are not completely relevant from high level API. --- client/ayon_core/pipeline/__init__.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index 0a805b16dc..41bcd0dbd1 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -42,16 +42,6 @@ from .load import ( register_loader_plugin_path, deregister_loader_plugin, - register_loader_pre_hook_plugin, - deregister_loader_pre_hook_plugin, - register_loader_pre_hook_plugin_path, - deregister_loader_pre_hook_plugin_path, - - register_loader_post_hook_plugin, - deregister_loader_post_hook_plugin, - register_loader_post_hook_plugin_path, - deregister_loader_post_hook_plugin_path, - load_container, remove_container, update_container, @@ -61,7 +51,6 @@ from .load import ( get_representation_path, get_representation_context, get_repres_contexts, - get_hook_loaders_by_identifier ) from .publish import ( @@ -171,16 +160,6 @@ __all__ = ( "register_loader_plugin_path", "deregister_loader_plugin", - "register_loader_pre_hook_plugin", - "deregister_loader_pre_hook_plugin", - "register_loader_pre_hook_plugin_path", - "deregister_loader_pre_hook_plugin_path", - - "register_loader_post_hook_plugin", - "deregister_loader_post_hook_plugin", - "register_loader_post_hook_plugin_path", - "deregister_loader_post_hook_plugin_path", - "load_container", "remove_container", "update_container", @@ -241,8 +220,6 @@ __all__ = ( "register_workfile_build_plugin_path", "deregister_workfile_build_plugin_path", - "get_hook_loaders_by_identifier", - # Backwards compatible function names "install", "uninstall", From 4457a432cb382e7a21fa8202e3ab1b2f524b7239 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:19:19 +0200 Subject: [PATCH 045/108] Merged pre/post hooks into single class --- client/ayon_core/pipeline/load/__init__.py | 27 +++++----------- client/ayon_core/pipeline/load/plugins.py | 36 ++++++---------------- 2 files changed, 18 insertions(+), 45 deletions(-) diff --git a/client/ayon_core/pipeline/load/__init__.py b/client/ayon_core/pipeline/load/__init__.py index eaba8cd78d..2a33fa119b 100644 --- a/client/ayon_core/pipeline/load/__init__.py +++ b/client/ayon_core/pipeline/load/__init__.py @@ -24,7 +24,6 @@ from .utils import ( get_loader_identifier, get_loaders_by_name, - get_hook_loaders_by_identifier, get_representation_path_from_context, get_representation_path, @@ -51,15 +50,10 @@ from .plugins import ( register_loader_plugin_path, deregister_loader_plugin, - register_loader_pre_hook_plugin, - deregister_loader_pre_hook_plugin, - register_loader_pre_hook_plugin_path, - deregister_loader_pre_hook_plugin_path, - - register_loader_post_hook_plugin, - deregister_loader_post_hook_plugin, - register_loader_post_hook_plugin_path, - deregister_loader_post_hook_plugin_path, + register_loader_hook_plugin, + deregister_loader_hook_plugin, + register_loader_hook_plugin_path, + deregister_loader_hook_plugin_path, ) @@ -90,7 +84,6 @@ __all__ = ( "get_loader_identifier", "get_loaders_by_name", - "get_hook_loaders_by_identifier", "get_representation_path_from_context", "get_representation_path", @@ -116,13 +109,9 @@ __all__ = ( "register_loader_plugin_path", "deregister_loader_plugin", - "register_loader_pre_hook_plugin", - "deregister_loader_pre_hook_plugin", - "register_loader_pre_hook_plugin_path", - "deregister_loader_pre_hook_plugin_path", + "register_loader_hook_plugin", + "deregister_loader_hook_plugin", + "register_loader_hook_plugin_path", + "deregister_loader_hook_plugin_path", - "register_loader_post_hook_plugin", - "deregister_loader_post_hook_plugin", - "register_loader_post_hook_plugin_path", - "deregister_loader_post_hook_plugin_path", ) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index ed26e5fe74..39133bc342 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -253,8 +253,8 @@ class ProductLoaderPlugin(LoaderPlugin): """ -class PreLoaderHookPlugin: - """Plugin that should be run before any Loaders in 'loaders' +class PrePostLoaderHookPlugin: + """Plugin that should be run before or post specific Loader in 'loaders' Should be used as non-invasive method to enrich core loading process. Any studio might want to modify loaded data before or after @@ -336,33 +336,17 @@ def register_loader_plugin_path(path): return register_plugin_path(LoaderPlugin, path) -def register_loader_pre_hook_plugin(plugin): - return register_plugin(PreLoaderHookPlugin, plugin) +def register_loader_hook_plugin(plugin): + return register_plugin(PrePostLoaderHookPlugin, plugin) -def deregister_loader_pre_hook_plugin(plugin): - deregister_plugin(PreLoaderHookPlugin, plugin) +def deregister_loader_hook_plugin(plugin): + deregister_plugin(PrePostLoaderHookPlugin, plugin) -def register_loader_pre_hook_plugin_path(path): - return register_plugin_path(PreLoaderHookPlugin, path) +def register_loader_hook_plugin_path(path): + return register_plugin_path(PrePostLoaderHookPlugin, path) -def deregister_loader_pre_hook_plugin_path(path): - deregister_plugin_path(PreLoaderHookPlugin, path) - - -def register_loader_post_hook_plugin(plugin): - return register_plugin(PostLoaderHookPlugin, plugin) - - -def deregister_loader_post_hook_plugin(plugin): - deregister_plugin(PostLoaderHookPlugin, plugin) - - -def register_loader_post_hook_plugin_path(path): - return register_plugin_path(PostLoaderHookPlugin, path) - - -def deregister_loader_post_hook_plugin_path(path): - deregister_plugin_path(PostLoaderHookPlugin, path) +def deregister_loader_hook_plugin_path(path): + deregister_plugin_path(PrePostLoaderHookPlugin, path) From ca768aeddf81a0cb7e1e76fd7326ca10ea1e2c5d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:21:56 +0200 Subject: [PATCH 046/108] Removed get_hook_loaders_by_identifier Replaced by monkeypatch load method --- client/ayon_core/pipeline/load/utils.py | 37 ------------------- .../ayon_core/tools/loader/models/actions.py | 25 +------------ .../ayon_core/tools/sceneinventory/control.py | 13 ------- .../sceneinventory/switch_dialog/dialog.py | 2 - client/ayon_core/tools/sceneinventory/view.py | 2 - 5 files changed, 2 insertions(+), 77 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 186cc8f0d7..fba4a8d2a8 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -293,7 +293,6 @@ def load_with_repre_context( namespace=None, name=None, options=None, - hooks=None, **kwargs ): @@ -338,7 +337,6 @@ def load_with_product_context( namespace=None, name=None, options=None, - hooks=None, **kwargs ): @@ -373,7 +371,6 @@ def load_with_product_contexts( namespace=None, name=None, options=None, - hooks=None, **kwargs ): @@ -1178,37 +1175,3 @@ def filter_containers(containers, project_name): uptodate_containers.append(container) return output - - -def get_hook_loaders_by_identifier(): - """Discovers pre/post hooks for loader plugins. - - Returns: - (dict) {"LoaderName": {"pre": ["PreLoader1"], "post":["PreLoader2]} - """ - # beware of circular imports! - from .plugins import PreLoadHookPlugin, PostLoadHookPlugin - - hook_loaders_by_identifier = {} - _get_hook_loaders(hook_loaders_by_identifier, PreLoadHookPlugin, "pre") - _get_hook_loaders(hook_loaders_by_identifier, PostLoadHookPlugin, "post") - return hook_loaders_by_identifier - - -def _get_hook_loaders(hook_loaders_by_identifier, loader_plugin, loader_type): - from ..plugin_discover import discover - - load_hook_plugins = discover(loader_plugin) - loaders_by_name = get_loaders_by_name() - for hook_plugin_cls in load_hook_plugins: - for load_plugin_name in hook_plugin_cls.loader_identifiers: - load_plugin = loaders_by_name.get(load_plugin_name) - if not load_plugin: - continue - if not load_plugin.enabled: - continue - identifier = get_loader_identifier(load_plugin) - (hook_loaders_by_identifier.setdefault(identifier, {}) - .setdefault(loader_type, []).append( - hook_plugin_cls) - ) diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index c2444da456..6ce9b0836d 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -17,8 +17,6 @@ from ayon_core.pipeline.load import ( load_with_product_contexts, LoadError, IncompatibleLoaderError, - get_loaders_by_name, - get_hook_loaders_by_identifier ) from ayon_core.tools.loader.abstract import ActionItem @@ -149,14 +147,12 @@ class LoaderActionsModel: ACTIONS_MODEL_SENDER, ) loader = self._get_loader_by_identifier(project_name, identifier) - hooks = self._get_hook_loaders_by_identifier(project_name, identifier) if representation_ids is not None: error_info = self._trigger_representation_loader( loader, options, project_name, representation_ids, - hooks ) elif version_ids is not None: error_info = self._trigger_version_loader( @@ -164,7 +160,6 @@ class LoaderActionsModel: options, project_name, version_ids, - hooks ) else: raise NotImplementedError( @@ -336,7 +331,6 @@ class LoaderActionsModel: available_loaders = self._filter_loaders_by_tool_name( project_name, discover_loader_plugins(project_name) ) - hook_loaders_by_identifier = get_hook_loaders_by_identifier() repre_loaders = [] product_loaders = [] loaders_by_identifier = {} @@ -354,7 +348,6 @@ class LoaderActionsModel: loaders_by_identifier_c.update_data(loaders_by_identifier) product_loaders_c.update_data(product_loaders) repre_loaders_c.update_data(repre_loaders) - hook_loaders_by_identifier_c.update_data(hook_loaders_by_identifier) return product_loaders, repre_loaders @@ -365,13 +358,6 @@ class LoaderActionsModel: loaders_by_identifier = loaders_by_identifier_c.get_data() return loaders_by_identifier.get(identifier) - def _get_hook_loaders_by_identifier(self, project_name, identifier): - if not self._hook_loaders_by_identifier[project_name].is_valid: - self._get_loaders(project_name) - hook_loaders_by_identifier_c = self._hook_loaders_by_identifier[project_name] - hook_loaders_by_identifier_c = hook_loaders_by_identifier_c.get_data() - return hook_loaders_by_identifier_c.get(identifier) - def _actions_sorter(self, action_item): """Sort the Loaders by their order and then their name. @@ -629,7 +615,6 @@ class LoaderActionsModel: options, project_name, version_ids, - hooks=None ): """Trigger version loader. @@ -679,7 +664,7 @@ class LoaderActionsModel: }) return self._load_products_by_loader( - loader, product_contexts, options, hooks=hooks + loader, product_contexts, options ) def _trigger_representation_loader( @@ -688,7 +673,6 @@ class LoaderActionsModel: options, project_name, representation_ids, - hooks ): """Trigger representation loader. @@ -741,7 +725,7 @@ class LoaderActionsModel: }) return self._load_representations_by_loader( - loader, repre_contexts, options, hooks + loader, repre_contexts, options ) def _load_representations_by_loader( @@ -749,7 +733,6 @@ class LoaderActionsModel: loader, repre_contexts, options, - hooks=None ): """Loops through list of repre_contexts and loads them with one loader @@ -773,7 +756,6 @@ class LoaderActionsModel: loader, repre_context, options=options, - hooks=hooks ) except IncompatibleLoaderError as exc: @@ -808,7 +790,6 @@ class LoaderActionsModel: loader, version_contexts, options, - hooks=None ): """Triggers load with ProductLoader type of loaders. @@ -834,7 +815,6 @@ class LoaderActionsModel: loader, version_contexts, options=options, - hooks=hooks ) except Exception as exc: formatted_traceback = None @@ -860,7 +840,6 @@ class LoaderActionsModel: loader, version_context, options=options, - hooks=hooks ) except Exception as exc: diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index c4e59699c3..60d9bc77a9 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -5,7 +5,6 @@ from ayon_core.host import HostBase from ayon_core.pipeline import ( registered_host, get_current_context, - get_hook_loaders_by_identifier ) from ayon_core.tools.common_models import HierarchyModel, ProjectsModel @@ -36,8 +35,6 @@ class SceneInventoryController: self._projects_model = ProjectsModel(self) self._event_system = self._create_event_system() - self._hooks_by_identifier = None - def get_host(self) -> HostBase: return self._host @@ -118,16 +115,6 @@ class SceneInventoryController: return self._containers_model.get_version_items( project_name, product_ids) - def get_hook_loaders_by_identifier(self): - """Returns lists of pre|post hooks per Loader identifier. - - Returns: - (dict) {"LoaderName": {"pre": ["PreLoader1"], "post":["PreLoader2]} - """ - if self._hooks_by_identifier is None: - self._hooks_by_identifier = get_hook_loaders_by_identifier() - return self._hooks_by_identifier - # Site Sync methods def is_sitesync_enabled(self): return self._sitesync_model.is_sitesync_enabled() diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py index c878cad079..f1b144f2aa 100644 --- a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py +++ b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py @@ -1339,13 +1339,11 @@ class SwitchAssetDialog(QtWidgets.QDialog): repre_entity = repres_by_name[container_repre_name] error = None - hook_loaders_by_id = self._controller.get_hook_loaders_by_identifier() try: switch_container( container, repre_entity, loader, - hook_loaders_by_id ) except ( LoaderSwitchNotImplementedError, diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 75d3d9a680..42338fe33b 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -1100,7 +1100,6 @@ class SceneInventoryView(QtWidgets.QTreeView): containers_by_id = self._controller.get_containers_by_item_ids( item_ids ) - hook_loaders_by_id = self._controller.get_hook_loaders_by_identifier() try: for item_id, item_version in zip(item_ids, versions): container = containers_by_id[item_id] @@ -1108,7 +1107,6 @@ class SceneInventoryView(QtWidgets.QTreeView): update_container( container, item_version, - hook_loaders_by_id ) except AssertionError: log.warning("Update failed", exc_info=True) From 37f5f5583250c122e15898f5b0b33374b3f89b57 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:22:24 +0200 Subject: [PATCH 047/108] Added typing --- client/ayon_core/pipeline/load/plugins.py | 36 ++++++++--------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 39133bc342..56b6a84706 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -265,33 +265,23 @@ class PrePostLoaderHookPlugin: def process(self, context, name=None, namespace=None, options=None): pass - def update(self, container, context): - pass - - def switch(self, container, context): - pass - - -class PostLoaderHookPlugin: - """Plugin that should be run after any Loaders in 'loaders' - - Should be used as non-invasive method to enrich core loading process. - Any external studio might want to modify loaded data before or after - they are loaded without need to override existing core plugins. - """ - loader_identifiers: ClassVar[set[str]] - - def process( + def pre_process( self, - container, - context, - name=None, - namespace=None, - options=None + context: dict, + name: str | None = None, + namespace: str | None = None, + options: dict | None = None, ): pass - def update(self, container, context): + def post_process( + self, + container: dict, # (ayon:container-3.0) + context: dict, + name: str | None = None, + namespace: str | None = None, + options: dict | None = None + ): pass def switch(self, container, context): From b742dfc381cfca3a8584a126aae67529557b5f43 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:22:44 +0200 Subject: [PATCH 048/108] Changed from loader_identifiers to is_compatible method --- client/ayon_core/pipeline/load/plugins.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 56b6a84706..c72f697e8b 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -260,9 +260,8 @@ class PrePostLoaderHookPlugin: Any studio might want to modify loaded data before or after they are loaded without need to override existing core plugins. """ - loader_identifiers: ClassVar[set[str]] - - def process(self, context, name=None, namespace=None, options=None): + @classmethod + def is_compatible(cls, Loader: LoaderPlugin) -> bool: pass def pre_process( From 976ef5fb2b8064efecfa4824e3d516b18bab197a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:23:22 +0200 Subject: [PATCH 049/108] Added monkey patched load method if hooks are found for loader --- client/ayon_core/pipeline/load/plugins.py | 32 +++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index c72f697e8b..695e4634f8 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -292,10 +292,11 @@ def discover_loader_plugins(project_name=None): from ayon_core.pipeline import get_current_project_name log = Logger.get_logger("LoaderDiscover") - plugins = discover(LoaderPlugin) if not project_name: project_name = get_current_project_name() project_settings = get_project_settings(project_name) + plugins = discover(LoaderPlugin) + hooks = discover(PrePostLoaderHookPlugin) for plugin in plugins: try: plugin.apply_settings(project_settings) @@ -304,11 +305,38 @@ def discover_loader_plugins(project_name=None): "Failed to apply settings to loader {}".format( plugin.__name__ ), - exc_info=True + exc_info=True, ) + for Hook in hooks: + if Hook.is_compatible(plugin): + hook_loader_load(plugin, Hook()) return plugins +def hook_loader_load(loader_class, hook_instance): + # If this is the first hook being added, wrap the original load method + if not hasattr(loader_class, '_load_hooks'): + loader_class._load_hooks = [] + + original_load = loader_class.load + + def wrapped_load(self, *args, **kwargs): + # Call pre_load on all hooks + for hook in loader_class._load_hooks: + hook.pre_load(*args, **kwargs) + # Call original load + result = original_load(self, *args, **kwargs) + # Call post_load on all hooks + for hook in loader_class._load_hooks: + hook.post_load(*args, **kwargs) + return result + + loader_class.load = wrapped_load + + # Add the new hook instance to the list + loader_class._load_hooks.append(hook_instance) + + def register_loader_plugin(plugin): return register_plugin(LoaderPlugin, plugin) From 4c113ca5b50e5afbdcfd1291bd637d0c3b665026 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:28:02 +0200 Subject: [PATCH 050/108] Removed unneeded _load_context --- client/ayon_core/pipeline/load/utils.py | 51 ++----------------------- 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index fba4a8d2a8..02a48dc6b6 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -321,14 +321,7 @@ def load_with_repre_context( ) loader = Loader() - return _load_context( - Loader, - repre_context, - name, - namespace, - options, - hooks - ) + loader.load(repre_context, name, namespace, options) def load_with_product_context( @@ -355,14 +348,7 @@ def load_with_product_context( Loader.__name__, product_context["folder"]["path"] ) ) - return _load_context( - Loader, - product_context, - name, - namespace, - options, - hooks - ) + return Loader().load(product_context, name, namespace, options) def load_with_product_contexts( @@ -393,38 +379,7 @@ def load_with_product_contexts( Loader.__name__, joined_product_names ) ) - return _load_context( - Loader, - product_contexts, - name, - namespace, - options, - hooks - ) - - -def _load_context(Loader, contexts, name, namespace, options, hooks): - """Helper function to wrap hooks around generic load function. - - Only dynamic part is different context(s) to be loaded. - """ - for hook_plugin_cls in hooks.get("pre", []): - hook_plugin_cls().process( - contexts, - name, - namespace, - options, - ) - loaded_container = Loader().load(contexts, name, namespace, options) - for hook_plugin_cls in hooks.get("post", []): - hook_plugin_cls().process( - loaded_container, - contexts, - name, - namespace, - options, - ) - return loaded_container + return Loader().load(product_contexts, name, namespace, options) def load_container( From 55583e68f8c388a15514db16859225288426f1df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:29:14 +0200 Subject: [PATCH 051/108] Fix missing return --- client/ayon_core/pipeline/load/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 02a48dc6b6..75d9450003 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -321,7 +321,7 @@ def load_with_repre_context( ) loader = Loader() - loader.load(repre_context, name, namespace, options) + return loader.load(repre_context, name, namespace, options) def load_with_product_context( From 7e88916fcd8567da2b466a27bdd1b7096e1bc106 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:29:54 +0200 Subject: [PATCH 052/108] Reverted missing newline --- client/ayon_core/pipeline/load/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 75d9450003..45000b023b 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -348,6 +348,7 @@ def load_with_product_context( Loader.__name__, product_context["folder"]["path"] ) ) + return Loader().load(product_context, name, namespace, options) @@ -379,6 +380,7 @@ def load_with_product_contexts( Loader.__name__, joined_product_names ) ) + return Loader().load(product_contexts, name, namespace, options) From f21f4a5e016c0f02c187d92d7d4051714096a9d0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:33:09 +0200 Subject: [PATCH 053/108] Removed usage of hooks in update, switch --- client/ayon_core/pipeline/load/utils.py | 33 +++---------------------- 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 45000b023b..a61ffee95a 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -469,7 +469,7 @@ def remove_container(container): return Loader().remove(container) -def update_container(container, version=-1, hooks_by_identifier=None): +def update_container(container, version=-1): """Update a container""" from ayon_core.pipeline import get_current_project_name @@ -565,20 +565,7 @@ def update_container(container, version=-1, hooks_by_identifier=None): if not path or not os.path.exists(path): raise ValueError("Path {} doesn't exist".format(path)) - loader_identifier = get_loader_identifier(Loader) - hooks = hooks_by_identifier.get(loader_identifier, {}) - for hook_plugin_cls in hooks.get("pre", []): - hook_plugin_cls().update( - context, - container - ) - updated_container = Loader().update(container, context) - for hook_plugin_cls in hooks.get("post", []): - hook_plugin_cls().update( - context, - container - ) - return updated_container + return Loader().update(container, context) def switch_container( @@ -635,21 +622,7 @@ def switch_container( loader = loader_plugin(context) - loader_identifier = get_loader_identifier(loader) - hooks = hooks_by_identifier.get(loader_identifier, {}) - for hook_plugin_cls in hooks.get("pre", []): - hook_plugin_cls().switch( - context, - container - ) - switched_container = loader.switch(container, context) - for hook_plugin_cls in hooks.get("post", []): - hook_plugin_cls().switch( - context, - container - ) - - return switched_container + return loader.switch(container, context) def _fix_representation_context_compatibility(repre_context): From 07809b56ddcadf764182fbeb923d72bd59ec734c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:35:22 +0200 Subject: [PATCH 054/108] Removed _hook_loaders_by_identifier in actions --- client/ayon_core/tools/loader/models/actions.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index 6ce9b0836d..f55f303f21 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -51,8 +51,6 @@ class LoaderActionsModel: levels=1, lifetime=self.loaders_cache_lifetime) self._repre_loaders = NestedCacheItem( levels=1, lifetime=self.loaders_cache_lifetime) - self._hook_loaders_by_identifier = NestedCacheItem( - levels=1, lifetime=self.loaders_cache_lifetime) def reset(self): """Reset the model with all cached items.""" @@ -61,7 +59,6 @@ class LoaderActionsModel: self._loaders_by_identifier.reset() self._product_loaders.reset() self._repre_loaders.reset() - self._hook_loaders_by_identifier.reset() def get_versions_action_items(self, project_name, version_ids): """Get action items for given version ids. @@ -318,13 +315,8 @@ class LoaderActionsModel: loaders_by_identifier_c = self._loaders_by_identifier[project_name] product_loaders_c = self._product_loaders[project_name] repre_loaders_c = self._repre_loaders[project_name] - hook_loaders_by_identifier_c = self._hook_loaders_by_identifier[project_name] if loaders_by_identifier_c.is_valid: - return ( - product_loaders_c.get_data(), - repre_loaders_c.get_data(), - hook_loaders_by_identifier_c.get_data() - ) + return product_loaders_c.get_data(), repre_loaders_c.get_data() # Get all representation->loader combinations available for the # index under the cursor, so we can list the user the options. From 9a050d92006c268ac4a99ddce83ae31c87006832 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:35:32 +0200 Subject: [PATCH 055/108] Formatting change --- client/ayon_core/tools/loader/models/actions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index f55f303f21..9a1a19c682 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -724,7 +724,7 @@ class LoaderActionsModel: self, loader, repre_contexts, - options, + options ): """Loops through list of repre_contexts and loads them with one loader @@ -747,7 +747,7 @@ class LoaderActionsModel: load_with_repre_context( loader, repre_context, - options=options, + options=options ) except IncompatibleLoaderError as exc: @@ -781,7 +781,7 @@ class LoaderActionsModel: self, loader, version_contexts, - options, + options ): """Triggers load with ProductLoader type of loaders. @@ -806,7 +806,7 @@ class LoaderActionsModel: load_with_product_contexts( loader, version_contexts, - options=options, + options=options ) except Exception as exc: formatted_traceback = None @@ -831,7 +831,7 @@ class LoaderActionsModel: load_with_product_context( loader, version_context, - options=options, + options=options ) except Exception as exc: From c8cca23e48fe6cd52a7632b376bd3946e38e3d57 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:37:43 +0200 Subject: [PATCH 056/108] Revert unneeded change --- client/ayon_core/tools/sceneinventory/view.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 42338fe33b..bb95e37d4e 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -1104,10 +1104,7 @@ class SceneInventoryView(QtWidgets.QTreeView): for item_id, item_version in zip(item_ids, versions): container = containers_by_id[item_id] try: - update_container( - container, - item_version, - ) + update_container(container, item_version) except AssertionError: log.warning("Update failed", exc_info=True) self._show_version_error_dialog( From a4babae5f5432d6d01f68e44d2b5b6675e08365d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 16:38:29 +0200 Subject: [PATCH 057/108] Revert unneeded change --- .../ayon_core/tools/sceneinventory/switch_dialog/dialog.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py index f1b144f2aa..a6d88ed44a 100644 --- a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py +++ b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py @@ -1340,11 +1340,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): error = None try: - switch_container( - container, - repre_entity, - loader, - ) + switch_container(container, repre_entity, loader) except ( LoaderSwitchNotImplementedError, IncompatibleLoaderError, From 26e2b45c9c4f00425a261a5c46de9dfd2451d709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:41:12 +0200 Subject: [PATCH 058/108] Update client/ayon_core/tools/loader/abstract.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/loader/abstract.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index d6a4bf40cb..1c3e30a109 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -215,22 +215,21 @@ class VersionItem: def __init__( self, - *, version_id: str, version: int, is_hero: bool, product_id: str, - task_id: Optional[str] = None, - thumbnail_id: Optional[str] = None, - published_time: Optional[str] = None, - author: Optional[str] = None, - status: Optional[str] = None, - frame_range: Optional[str] = None, - duration: Optional[int] = None, - handles: Optional[str] = None, - step: Optional[int] = None, - comment: Optional[str] = None, - source: Optional[str] = None, + task_id: Optional[str], + thumbnail_id: Optional[str], + published_time: Optional[str], + author: Optional[str], + status: Optional[str], + frame_range: Optional[str], + duration: Optional[int], + handles: Optional[str], + step: Optional[int], + comment: Optional[str], + source: Optional[str], ): self.version_id = version_id self.product_id = product_id From 1d66a86d799863aa51e2161fd0dbed5e337a6426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 13 Jun 2025 16:41:22 +0200 Subject: [PATCH 059/108] Update client/ayon_core/tools/loader/abstract.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/loader/abstract.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 1c3e30a109..804956f875 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -144,7 +144,6 @@ class ProductItem: folder_id: str, folder_label: str, version_items: dict[str, VersionItem], - *, product_in_scene: bool, ): self.product_id = product_id From 0e292eb3560d9771ded3b4c9ab0198a59952ef25 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 17:43:47 +0200 Subject: [PATCH 060/108] Renamed --- client/ayon_core/pipeline/load/plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 695e4634f8..02479655eb 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -307,9 +307,9 @@ def discover_loader_plugins(project_name=None): ), exc_info=True, ) - for Hook in hooks: - if Hook.is_compatible(plugin): - hook_loader_load(plugin, Hook()) + for hookCls in hooks: + if hookCls.is_compatible(plugin): + hook_loader_load(plugin, hookCls()) return plugins From 276eff0097a2846cd7fd3497e22ff2f67ce88781 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 17:45:44 +0200 Subject: [PATCH 061/108] Added docstring --- client/ayon_core/pipeline/load/plugins.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 02479655eb..190b3d3a30 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -313,7 +313,11 @@ def discover_loader_plugins(project_name=None): return plugins -def hook_loader_load(loader_class, hook_instance): +def hook_loader_load( + loader_class: LoaderPlugin, + hook_instance: PrePostLoaderHookPlugin +) -> None: + """Monkey patch method replacing Loader.load method with wrapped hooks.""" # If this is the first hook being added, wrap the original load method if not hasattr(loader_class, '_load_hooks'): loader_class._load_hooks = [] From 118796a32523fedf60709437502f1ade8304dd7b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 17:46:41 +0200 Subject: [PATCH 062/108] Passed returned container from load as keyword Could be appended on position 0 on args, but this feels safer. --- client/ayon_core/pipeline/load/plugins.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 190b3d3a30..9040b122e3 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -264,7 +264,7 @@ class PrePostLoaderHookPlugin: def is_compatible(cls, Loader: LoaderPlugin) -> bool: pass - def pre_process( + def pre_load( self, context: dict, name: str | None = None, @@ -273,13 +273,13 @@ class PrePostLoaderHookPlugin: ): pass - def post_process( + def post_load( self, - container: dict, # (ayon:container-3.0) context: dict, name: str | None = None, namespace: str | None = None, - options: dict | None = None + options: dict | None = None, + container: dict | None = None, # (ayon:container-3.0) ): pass @@ -329,11 +329,12 @@ def hook_loader_load( for hook in loader_class._load_hooks: hook.pre_load(*args, **kwargs) # Call original load - result = original_load(self, *args, **kwargs) + container = original_load(self, *args, **kwargs) + kwargs["container"] = container # Call post_load on all hooks for hook in loader_class._load_hooks: hook.post_load(*args, **kwargs) - return result + return container loader_class.load = wrapped_load From 4ae7ab28ab0ff89ae930d5900307bfb2048d755e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 13 Jun 2025 17:55:12 +0200 Subject: [PATCH 063/108] Removed unnecessary import --- client/ayon_core/pipeline/load/plugins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 9040b122e3..808eaf9340 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,7 +1,6 @@ from __future__ import annotations import os import logging -from typing import ClassVar from ayon_core.settings import get_project_settings from ayon_core.pipeline.plugin_discover import ( From cf4f9cfea61b2cbb95aa6caef3fcda10723c36b9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jun 2025 14:04:47 +0200 Subject: [PATCH 064/108] Removed unused argument --- client/ayon_core/pipeline/load/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index a61ffee95a..3c50d76fb5 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -572,7 +572,6 @@ def switch_container( container, representation, loader_plugin=None, - hooks_by_identifier=None ): """Switch a container to representation @@ -580,7 +579,6 @@ def switch_container( container (dict): container information representation (dict): representation entity loader_plugin (LoaderPlugin) - hooks_by_identifier (dict): {"pre": [PreHookPlugin1], "post":[]} Returns: return from function call From 234e38869773507c0b9e8eb96a8d1f182e71a8e8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jun 2025 14:07:52 +0200 Subject: [PATCH 065/108] Added abstractmethod decorator --- client/ayon_core/pipeline/load/plugins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 808eaf9340..50423af051 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,6 +1,7 @@ from __future__ import annotations import os import logging +from abc import abstractmethod from ayon_core.settings import get_project_settings from ayon_core.pipeline.plugin_discover import ( @@ -260,9 +261,11 @@ class PrePostLoaderHookPlugin: they are loaded without need to override existing core plugins. """ @classmethod + @abstractmethod def is_compatible(cls, Loader: LoaderPlugin) -> bool: pass + @abstractmethod def pre_load( self, context: dict, @@ -272,6 +275,7 @@ class PrePostLoaderHookPlugin: ): pass + @abstractmethod def post_load( self, context: dict, From 0e582c7d5fbdf54e90b54f5398257495e7e7735e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jun 2025 14:08:26 +0200 Subject: [PATCH 066/108] Update docstring --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 50423af051..2268935099 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -254,7 +254,7 @@ class ProductLoaderPlugin(LoaderPlugin): class PrePostLoaderHookPlugin: - """Plugin that should be run before or post specific Loader in 'loaders' + """Plugin that runs before and post specific Loader in 'loaders' Should be used as non-invasive method to enrich core loading process. Any studio might want to modify loaded data before or after From 03e3b29597c7c7ccab83e1777f7be13d4d419b3e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jun 2025 14:22:08 +0200 Subject: [PATCH 067/108] Renamed variable --- client/ayon_core/pipeline/load/plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 2268935099..4cf497836c 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -310,9 +310,9 @@ def discover_loader_plugins(project_name=None): ), exc_info=True, ) - for hookCls in hooks: - if hookCls.is_compatible(plugin): - hook_loader_load(plugin, hookCls()) + for hook_cls in hooks: + if hook_cls.is_compatible(plugin): + hook_loader_load(plugin, hook_cls()) return plugins From f4556ac697ea9b127451b661f8c0e6c9f546bdd2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jun 2025 15:58:03 +0200 Subject: [PATCH 068/108] Formatting change --- client/ayon_core/tools/loader/models/actions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index 9a1a19c682..40331d73a4 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -17,7 +17,6 @@ from ayon_core.pipeline.load import ( load_with_product_contexts, LoadError, IncompatibleLoaderError, - ) from ayon_core.tools.loader.abstract import ActionItem @@ -743,7 +742,6 @@ class LoaderActionsModel: if version < 0: version = "Hero" try: - load_with_repre_context( loader, repre_context, From 77d5d8d162fcccf322f635e56d5498dac7d6164a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jun 2025 17:33:30 +0200 Subject: [PATCH 069/108] Implemented order attribute for sorting --- client/ayon_core/pipeline/load/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 4cf497836c..9f3d9531c7 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -260,6 +260,8 @@ class PrePostLoaderHookPlugin: Any studio might want to modify loaded data before or after they are loaded without need to override existing core plugins. """ + order = 0 + @classmethod @abstractmethod def is_compatible(cls, Loader: LoaderPlugin) -> bool: @@ -300,6 +302,7 @@ def discover_loader_plugins(project_name=None): project_settings = get_project_settings(project_name) plugins = discover(LoaderPlugin) hooks = discover(PrePostLoaderHookPlugin) + sorted_hooks = sorted(hooks, key=lambda hook: hook.order) for plugin in plugins: try: plugin.apply_settings(project_settings) @@ -310,7 +313,7 @@ def discover_loader_plugins(project_name=None): ), exc_info=True, ) - for hook_cls in hooks: + for hook_cls in sorted_hooks: if hook_cls.is_compatible(plugin): hook_loader_load(plugin, hook_cls()) return plugins From 802b8b2567ccd13067d1a14b86138781c02ae114 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jun 2025 17:38:15 +0200 Subject: [PATCH 070/108] Refactored monkey patched method --- client/ayon_core/pipeline/load/plugins.py | 41 +++++++++++------------ 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 9f3d9531c7..30e8856cf8 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -313,39 +313,36 @@ def discover_loader_plugins(project_name=None): ), exc_info=True, ) + compatible_hooks = [] for hook_cls in sorted_hooks: if hook_cls.is_compatible(plugin): - hook_loader_load(plugin, hook_cls()) + compatible_hooks.append(hook_cls) + add_hooks_to_loader(plugin, compatible_hooks) return plugins -def hook_loader_load( +def add_hooks_to_loader( loader_class: LoaderPlugin, - hook_instance: PrePostLoaderHookPlugin + compatible_hooks: list[PrePostLoaderHookPlugin] ) -> None: """Monkey patch method replacing Loader.load method with wrapped hooks.""" - # If this is the first hook being added, wrap the original load method - if not hasattr(loader_class, '_load_hooks'): - loader_class._load_hooks = [] + loader_class._load_hooks = compatible_hooks - original_load = loader_class.load + original_load = loader_class.load - def wrapped_load(self, *args, **kwargs): - # Call pre_load on all hooks - for hook in loader_class._load_hooks: - hook.pre_load(*args, **kwargs) - # Call original load - container = original_load(self, *args, **kwargs) - kwargs["container"] = container - # Call post_load on all hooks - for hook in loader_class._load_hooks: - hook.post_load(*args, **kwargs) - return container + def wrapped_load(self, *args, **kwargs): + # Call pre_load on all hooks + for hook in loader_class._load_hooks: + hook.pre_load(*args, **kwargs) + # Call original load + container = original_load(self, *args, **kwargs) + kwargs["container"] = container + # Call post_load on all hooks + for hook in loader_class._load_hooks: + hook.post_load(*args, **kwargs) + return container - loader_class.load = wrapped_load - - # Add the new hook instance to the list - loader_class._load_hooks.append(hook_instance) + loader_class.load = wrapped_load def register_loader_plugin(plugin): From 3db2cd046a1f8755b51a2473bd8af2d138385083 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 17 Jun 2025 14:44:24 +0200 Subject: [PATCH 071/108] Implemented pre/post for update and remove --- client/ayon_core/pipeline/load/plugins.py | 80 ++++++++++++++++++----- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 30e8856cf8..76cc8bd19b 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -288,7 +288,33 @@ class PrePostLoaderHookPlugin: ): pass - def switch(self, container, context): + @abstractmethod + def pre_update( + self, + container: dict, # (ayon:container-3.0) + context: dict, + ): + pass + + @abstractmethod + def post_update( + container: dict, # (ayon:container-3.0) + context: dict, + ): + pass + + @abstractmethod + def pre_remove( + self, + container: dict, # (ayon:container-3.0) + ): + pass + + @abstractmethod + def post_remove( + container: dict, # (ayon:container-3.0) + context: dict, + ): pass @@ -322,27 +348,47 @@ def discover_loader_plugins(project_name=None): def add_hooks_to_loader( - loader_class: LoaderPlugin, - compatible_hooks: list[PrePostLoaderHookPlugin] + loader_class: LoaderPlugin, compatible_hooks: list[PrePostLoaderHookPlugin] ) -> None: - """Monkey patch method replacing Loader.load method with wrapped hooks.""" + """Monkey patch method replacing Loader.load|update|remove methods + + It wraps applicable loaders with pre/post hooks. Discovery is called only + once per loaders discovery. + """ loader_class._load_hooks = compatible_hooks - original_load = loader_class.load + def wrap_method(method_name: str): + original_method = getattr(loader_class, method_name) - def wrapped_load(self, *args, **kwargs): - # Call pre_load on all hooks - for hook in loader_class._load_hooks: - hook.pre_load(*args, **kwargs) - # Call original load - container = original_load(self, *args, **kwargs) - kwargs["container"] = container - # Call post_load on all hooks - for hook in loader_class._load_hooks: - hook.post_load(*args, **kwargs) - return container + def wrapped_method(self, *args, **kwargs): + # Call pre_ on all hooks + pre_hook_name = f"pre_{method_name}" + for hook in loader_class._load_hooks: + pre_hook = getattr(hook, pre_hook_name, None) + if callable(pre_hook): + pre_hook(self, *args, **kwargs) - loader_class.load = wrapped_load + # Call original method + result = original_method(self, *args, **kwargs) + + # Add result to kwargs if needed + # Assuming container-like result for load, update, remove + kwargs["container"] = result + + # Call post_ on all hooks + post_hook_name = f"post_{method_name}" + for hook in loader_class._load_hooks: + post_hook = getattr(hook, post_hook_name, None) + if callable(post_hook): + post_hook(self, *args, **kwargs) + + return result + + setattr(loader_class, method_name, wrapped_method) + + for method in ("load", "update", "remove"): + if hasattr(loader_class, method): + wrap_method(method) def register_loader_plugin(plugin): From 3018dd35b68825f0cff49744713511ca583bd217 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jun 2025 21:58:16 +0200 Subject: [PATCH 072/108] Refactor `PrePostLoaderHookPlugin` to `LoaderHookPlugin` --- client/ayon_core/pipeline/load/plugins.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 76cc8bd19b..ca3cdce5e3 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -253,7 +253,7 @@ class ProductLoaderPlugin(LoaderPlugin): """ -class PrePostLoaderHookPlugin: +class LoaderHookPlugin: """Plugin that runs before and post specific Loader in 'loaders' Should be used as non-invasive method to enrich core loading process. @@ -327,7 +327,7 @@ def discover_loader_plugins(project_name=None): project_name = get_current_project_name() project_settings = get_project_settings(project_name) plugins = discover(LoaderPlugin) - hooks = discover(PrePostLoaderHookPlugin) + hooks = discover(LoaderHookPlugin) sorted_hooks = sorted(hooks, key=lambda hook: hook.order) for plugin in plugins: try: @@ -348,7 +348,7 @@ def discover_loader_plugins(project_name=None): def add_hooks_to_loader( - loader_class: LoaderPlugin, compatible_hooks: list[PrePostLoaderHookPlugin] + loader_class: LoaderPlugin, compatible_hooks: list[LoaderHookPlugin] ) -> None: """Monkey patch method replacing Loader.load|update|remove methods @@ -408,16 +408,16 @@ def register_loader_plugin_path(path): def register_loader_hook_plugin(plugin): - return register_plugin(PrePostLoaderHookPlugin, plugin) + return register_plugin(LoaderHookPlugin, plugin) def deregister_loader_hook_plugin(plugin): - deregister_plugin(PrePostLoaderHookPlugin, plugin) + deregister_plugin(LoaderHookPlugin, plugin) def register_loader_hook_plugin_path(path): - return register_plugin_path(PrePostLoaderHookPlugin, path) + return register_plugin_path(LoaderHookPlugin, path) def deregister_loader_hook_plugin_path(path): - deregister_plugin_path(PrePostLoaderHookPlugin, path) + deregister_plugin_path(LoaderHookPlugin, path) From 85ef0fefa41cfbdbe9dfea21cfc390fab69258b1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jun 2025 21:59:06 +0200 Subject: [PATCH 073/108] Fix `post_update` and `post_remove` --- client/ayon_core/pipeline/load/plugins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index ca3cdce5e3..7176bd82ac 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -298,6 +298,7 @@ class LoaderHookPlugin: @abstractmethod def post_update( + self, container: dict, # (ayon:container-3.0) context: dict, ): @@ -312,8 +313,8 @@ class LoaderHookPlugin: @abstractmethod def post_remove( + self, container: dict, # (ayon:container-3.0) - context: dict, ): pass From bcca8ca8c1d847dc5064c37ef8a790c415a451b4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jun 2025 22:41:31 +0200 Subject: [PATCH 074/108] Rename `post_*` method kwarg `container` to `result` to not clash with `container` argument on `update` and `remove` and make it clearer that it's the "result" of something --- client/ayon_core/pipeline/load/plugins.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 7176bd82ac..0fc2718190 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,6 +1,7 @@ from __future__ import annotations import os import logging +from typing import Any from abc import abstractmethod from ayon_core.settings import get_project_settings @@ -259,6 +260,9 @@ class LoaderHookPlugin: Should be used as non-invasive method to enrich core loading process. Any studio might want to modify loaded data before or after they are loaded without need to override existing core plugins. + + The post methods are called after the loader's methods and receive the + return value of the loader's method as `result` argument. """ order = 0 @@ -284,7 +288,7 @@ class LoaderHookPlugin: name: str | None = None, namespace: str | None = None, options: dict | None = None, - container: dict | None = None, # (ayon:container-3.0) + result: Any = None, ): pass @@ -301,6 +305,7 @@ class LoaderHookPlugin: self, container: dict, # (ayon:container-3.0) context: dict, + result: Any = None, ): pass @@ -315,6 +320,7 @@ class LoaderHookPlugin: def post_remove( self, container: dict, # (ayon:container-3.0) + result: Any = None, ): pass @@ -374,7 +380,7 @@ def add_hooks_to_loader( # Add result to kwargs if needed # Assuming container-like result for load, update, remove - kwargs["container"] = result + kwargs["result"] = result # Call post_ on all hooks post_hook_name = f"post_{method_name}" From d583279c6dad91ecb5e72218455560d66afe6697 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jun 2025 23:26:30 +0200 Subject: [PATCH 075/108] Fix type hint --- client/ayon_core/pipeline/load/plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 0fc2718190..4582c2b329 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,7 +1,7 @@ from __future__ import annotations import os import logging -from typing import Any +from typing import Any, Type from abc import abstractmethod from ayon_core.settings import get_project_settings @@ -268,7 +268,7 @@ class LoaderHookPlugin: @classmethod @abstractmethod - def is_compatible(cls, Loader: LoaderPlugin) -> bool: + def is_compatible(cls, Loader: Type[LoaderPlugin]) -> bool: pass @abstractmethod From a593516f29394b28f03cb14e8d2d1818ebbc9ca0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Jun 2025 23:39:25 +0200 Subject: [PATCH 076/108] Fix Hook logic to actually run on a `LoaderHookPlugin` instance - Now `self` on the hook method actually refers to an instantiated `LoaderHookPlugin` - Add `plugin` kwargs to the hook methods to still provide access to the `LoaderPlugin` instance for potential advanced behavior - Fix type hint on `compatible_hooks` arguments to `add_hooks_to_loader` function --- client/ayon_core/pipeline/load/plugins.py | 30 +++++++++++++++++------ 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 4582c2b329..4f0071b4d3 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -278,6 +278,7 @@ class LoaderHookPlugin: name: str | None = None, namespace: str | None = None, options: dict | None = None, + plugin: LoaderPlugin | None = None, ): pass @@ -288,6 +289,7 @@ class LoaderHookPlugin: name: str | None = None, namespace: str | None = None, options: dict | None = None, + plugin: LoaderPlugin | None = None, result: Any = None, ): pass @@ -297,6 +299,8 @@ class LoaderHookPlugin: self, container: dict, # (ayon:container-3.0) context: dict, + plugin: LoaderPlugin | None = None, + ): pass @@ -305,6 +309,7 @@ class LoaderHookPlugin: self, container: dict, # (ayon:container-3.0) context: dict, + plugin: LoaderPlugin | None = None, result: Any = None, ): pass @@ -313,6 +318,7 @@ class LoaderHookPlugin: def pre_remove( self, container: dict, # (ayon:container-3.0) + plugin: LoaderPlugin | None = None, ): pass @@ -320,6 +326,7 @@ class LoaderHookPlugin: def post_remove( self, container: dict, # (ayon:container-3.0) + plugin: LoaderPlugin | None = None, result: Any = None, ): pass @@ -355,7 +362,7 @@ def discover_loader_plugins(project_name=None): def add_hooks_to_loader( - loader_class: LoaderPlugin, compatible_hooks: list[LoaderHookPlugin] + loader_class: LoaderPlugin, compatible_hooks: list[Type[LoaderHookPlugin]] ) -> None: """Monkey patch method replacing Loader.load|update|remove methods @@ -370,24 +377,31 @@ def add_hooks_to_loader( def wrapped_method(self, *args, **kwargs): # Call pre_ on all hooks pre_hook_name = f"pre_{method_name}" - for hook in loader_class._load_hooks: + + # Pass the LoaderPlugin instance to the hooks + hook_kwargs = kwargs.copy() + hook_kwargs["plugin"] = self + + hooks: list[LoaderHookPlugin] = [] + for Hook in loader_class._load_hooks: + hook = Hook() # Instantiate the hook + hooks.append(hook) pre_hook = getattr(hook, pre_hook_name, None) if callable(pre_hook): - pre_hook(self, *args, **kwargs) + pre_hook(*args, **hook_kwargs) # Call original method result = original_method(self, *args, **kwargs) - # Add result to kwargs if needed - # Assuming container-like result for load, update, remove - kwargs["result"] = result + # Add result to kwargs for post hooks from the original method + hook_kwargs["result"] = result # Call post_ on all hooks post_hook_name = f"post_{method_name}" - for hook in loader_class._load_hooks: + for hook in hooks: post_hook = getattr(hook, post_hook_name, None) if callable(post_hook): - post_hook(self, *args, **kwargs) + post_hook(*args, **hook_kwargs) return result From 7ce23aff0776c067e840263ed72bc22073922abc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 08:56:37 +0200 Subject: [PATCH 077/108] added pinned projects to project item --- .../ayon_core/tools/common_models/projects.py | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 7ec941e6bd..9bbdc8a75c 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -1,6 +1,8 @@ +from __future__ import annotations import contextlib from abc import ABC, abstractmethod from typing import Dict, Any +from dataclasses import dataclass import ayon_api @@ -140,6 +142,7 @@ class TaskTypeItem: ) +@dataclass class ProjectItem: """Item representing folder entity on a server. @@ -150,21 +153,14 @@ class ProjectItem: active (Union[str, None]): Parent folder id. If 'None' then project is parent. """ - - def __init__(self, name, active, is_library, icon=None): - self.name = name - self.active = active - self.is_library = is_library - if icon is None: - icon = { - "type": "awesome-font", - "name": "fa.book" if is_library else "fa.map", - "color": get_default_entity_icon_color(), - } - self.icon = icon + name: str + active: bool + is_library: bool + icon: dict[str, Any] + is_pinned: bool = False @classmethod - def from_entity(cls, project_entity): + def from_entity(cls, project_entity: dict[str, Any]) -> "ProjectItem": """Creates folder item from entity. Args: @@ -174,10 +170,16 @@ class ProjectItem: ProjectItem: Project item. """ + icon = { + "type": "awesome-font", + "name": "fa.book" if project_entity["library"] else "fa.map", + "color": get_default_entity_icon_color(), + } return cls( project_entity["name"], project_entity["active"], project_entity["library"], + icon ) def to_data(self): @@ -208,16 +210,18 @@ class ProjectItem: return cls(**data) -def _get_project_items_from_entitiy(projects): +def _get_project_items_from_entitiy( + projects: list[dict[str, Any]] +) -> list[ProjectItem]: """ Args: projects (list[dict[str, Any]]): List of projects. Returns: - ProjectItem: Project item. - """ + list[ProjectItem]: Project item. + """ return [ ProjectItem.from_entity(project) for project in projects @@ -428,9 +432,19 @@ class ProjectsModel(object): self._projects_cache.update_data(project_items) return self._projects_cache.get_data() - def _query_projects(self): + def _query_projects(self) -> list[ProjectItem]: projects = ayon_api.get_projects(fields=["name", "active", "library"]) - return _get_project_items_from_entitiy(projects) + user = ayon_api.get_user() + pinned_projects = ( + user + .get("data", {}) + .get("frontendPreferences", {}) + .get("pinnedProjects") + ) or [] + project_items = _get_project_items_from_entitiy(list(projects)) + for project in project_items: + project.is_pinned = project.name in pinned_projects + return project_items def _status_items_getter(self, project_entity): if not project_entity: From 51116bb9bc444f4ff87799dd8385db4dcdb4dbfa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 08:57:32 +0200 Subject: [PATCH 078/108] sort and show the pinned project --- .../ayon_core/tools/utils/projects_widget.py | 244 ++++++++++++++++-- 1 file changed, 224 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/tools/utils/projects_widget.py b/client/ayon_core/tools/utils/projects_widget.py index c340be2f83..c3d0e4160a 100644 --- a/client/ayon_core/tools/utils/projects_widget.py +++ b/client/ayon_core/tools/utils/projects_widget.py @@ -1,21 +1,69 @@ +from __future__ import annotations +from abc import ABC, abstractmethod +from collections.abc import Callable +import typing +from typing import Optional + from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.tools.common_models import PROJECTS_MODEL_SENDER +from ayon_core.tools.common_models import ( + ProjectItem, + PROJECTS_MODEL_SENDER, +) from .lib import RefreshThread, get_qt_icon +if typing.TYPE_CHECKING: + from typing import TypedDict + + class ExpectedProjectSelectionData(TypedDict): + name: Optional[str] + current: Optional[str] + selected: Optional[str] + + + class ExpectedSelectionData(TypedDict): + project: ExpectedProjectSelectionData + + PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2 PROJECT_IS_LIBRARY_ROLE = QtCore.Qt.UserRole + 3 PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4 -LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5 +PROJECT_IS_PINNED_ROLE = QtCore.Qt.UserRole + 5 +LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 6 + + +class AbstractProjectController(ABC): + @abstractmethod + def register_event_callback(self, topic: str, callback: Callable): + pass + + @abstractmethod + def get_project_items( + self, sender: Optional[str] = None + ) -> list[str]: + pass + + @abstractmethod + def set_selected_project(self, project_name: str): + pass + + # These are required only if widget should handle expected selection + @abstractmethod + def expected_project_selected(self, project_name: str): + pass + + @abstractmethod + def get_expected_selection_data(self) -> "ExpectedSelectionData": + pass class ProjectsQtModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() - def __init__(self, controller): - super(ProjectsQtModel, self).__init__() + def __init__(self, controller: AbstractProjectController): + super().__init__() self._controller = controller self._project_items = {} @@ -213,7 +261,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel): else: self.refreshed.emit() - def _fill_items(self, project_items): + def _fill_items(self, project_items: list[ProjectItem]): new_project_names = { project_item.name for project_item in project_items @@ -252,6 +300,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel): item.setData(project_name, PROJECT_NAME_ROLE) item.setData(project_item.active, PROJECT_IS_ACTIVE_ROLE) item.setData(project_item.is_library, PROJECT_IS_LIBRARY_ROLE) + item.setData(project_item.is_pinned, PROJECT_IS_PINNED_ROLE) is_current = project_name == self._current_context_project item.setData(is_current, PROJECT_IS_CURRENT_ROLE) self._project_items[project_name] = item @@ -323,26 +372,52 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): return False # Library separator should be before library projects - result = self._type_sort(left_index, right_index) - if result is not None: - return result + l_is_library = left_index.data(PROJECT_IS_LIBRARY_ROLE) + r_is_library = right_index.data(PROJECT_IS_LIBRARY_ROLE) + l_is_sep = left_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE) + r_is_sep = right_index.data(LIBRARY_PROJECT_SEPARATOR_ROLE) + if l_is_sep: + return bool(r_is_library) - if left_index.data(PROJECT_NAME_ROLE) is None: + if r_is_sep: + return not l_is_library + + # Non project items should be on top + l_project_name = left_index.data(PROJECT_NAME_ROLE) + r_project_name = right_index.data(PROJECT_NAME_ROLE) + if l_project_name is None: return True - - if right_index.data(PROJECT_NAME_ROLE) is None: + if r_project_name is None: return False left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE) right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE) - if right_is_active == left_is_active: - return super(ProjectSortFilterProxy, self).lessThan( - left_index, right_index - ) + if right_is_active != left_is_active: + return left_is_active - if left_is_active: + l_is_pinned = left_index.data(PROJECT_IS_PINNED_ROLE) + r_is_pinned = right_index.data(PROJECT_IS_PINNED_ROLE) + if l_is_pinned is True and not r_is_pinned: return True - return False + + if r_is_pinned is True and not l_is_pinned: + return False + + # Move inactive projects to the end + left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE) + right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE) + if right_is_active != left_is_active: + return left_is_active + + # Move library projects after standard projects + if ( + l_is_library is not None + and r_is_library is not None + and l_is_library != r_is_library + ): + return r_is_library + return super().lessThan(left_index, right_index) + def filterAcceptsRow(self, source_row, source_parent): index = self.sourceModel().index(source_row, 0, source_parent) @@ -415,15 +490,144 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): self.invalidate() +class ProjectsDelegate(QtWidgets.QStyledItemDelegate): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._pin_icon = None + + def paint(self, painter, option, index): + is_pinned = index.data(PROJECT_IS_PINNED_ROLE) + if not is_pinned: + super().paint(painter, option, index) + return + opt = QtWidgets.QStyleOptionViewItem(option) + self.initStyleOption(opt, index) + widget = option.widget + if widget is None: + style = QtWidgets.QApplication.style() + else: + style = widget.style() + # CE_ItemViewItem + proxy = style.proxy() + painter.save() + painter.setClipRect(option.rect) + decor_rect = proxy.subElementRect( + QtWidgets.QStyle.SE_ItemViewItemDecoration, opt, widget + ) + text_rect = proxy.subElementRect( + QtWidgets.QStyle.SE_ItemViewItemText, opt, widget + ) + proxy.drawPrimitive( + QtWidgets.QStyle.PE_PanelItemViewItem, opt, painter, widget + ) + mode = QtGui.QIcon.Normal + if not opt.state & QtWidgets.QStyle.State_Enabled: + mode = QtGui.QIcon.Disabled + elif opt.state & QtWidgets.QStyle.State_Selected: + mode = QtGui.QIcon.Selected + state = QtGui.QIcon.Off + if opt.state & QtWidgets.QStyle.State_Open: + state = QtGui.QIcon.On + + # Draw project icon + opt.icon.paint( + painter, decor_rect, opt.decorationAlignment, mode, state + ) + + # Draw pin icon + if index.data(PROJECT_IS_PINNED_ROLE): + pin_icon = self._get_pin_icon() + pin_rect = QtCore.QRect(decor_rect) + diff = option.rect.width() - pin_rect.width() + pin_rect.moveLeft(diff) + pin_icon.paint( + painter, pin_rect, opt.decorationAlignment, mode, state + ) + + # Draw text + if opt.text: + if not opt.state & QtWidgets.QStyle.State_Enabled: + cg = QtGui.QPalette.Disabled + elif not (opt.state & QtWidgets.QStyle.State_Active): + cg = QtGui.QPalette.Inactive + else: + cg = QtGui.QPalette.Normal + + if opt.state & QtWidgets.QStyle.State_Selected: + painter.setPen(opt.palette.color(cg, QtGui.QPalette.HighlightedText)) + else: + painter.setPen(opt.palette.color(cg, QtGui.QPalette.Text)) + + if opt.state & QtWidgets.QStyle.State_Editing: + painter.setPen(opt.palette.color(cg, QtGui.QPalette.Text)) + painter.drawRect(text_rect.adjusted(0, 0, -1, -1)) + + painter.drawText( + text_rect, + opt.displayAlignment, + opt.text + ) + + # Draw focus rect + if opt.state & QtWidgets.QStyle.State_HasFocus: + focus_opt = QtWidgets.QStyleOptionFocusRect() + focus_opt.state = option.state + focus_opt.direction = option.direction + focus_opt.rect = option.rect + focus_opt.fontMetrics = option.fontMetrics + focus_opt.palette = option.palette + + focus_opt.rect = style.subElementRect( + QtWidgets.QCommonStyle.SE_ItemViewItemFocusRect, + option, + option.widget + ) + focus_opt.state |= ( + QtWidgets.QStyle.State_KeyboardFocusChange + | QtWidgets.QStyle.State_Item + ) + focus_opt.backgroundColor = option.palette.color( + ( + QtGui.QPalette.Normal + if option.state & QtWidgets.QStyle.State_Enabled + else QtGui.QPalette.Disabled + ), + ( + QtGui.QPalette.Highlight + if option.state & QtWidgets.QStyle.State_Selected + else QtGui.QPalette.Window + ) + ) + style.drawPrimitive( + QtWidgets.QCommonStyle.PE_FrameFocusRect, + focus_opt, + painter, + option.widget + ) + painter.restore() + + def _get_pin_icon(self): + if self._pin_icon is None: + self._pin_icon = get_qt_icon({ + "type": "material-symbols", + "name": "keep", + }) + return self._pin_icon + class ProjectsCombobox(QtWidgets.QWidget): refreshed = QtCore.Signal() selection_changed = QtCore.Signal() - def __init__(self, controller, parent, handle_expected_selection=False): - super(ProjectsCombobox, self).__init__(parent) + def __init__( + self, + controller: AbstractProjectController, + parent: QtWidgets.QWidget, + handle_expected_selection: bool = False, + ): + super().__init__(parent) projects_combobox = QtWidgets.QComboBox(self) - combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox) + combobox_delegate = ProjectsDelegate(projects_combobox) projects_combobox.setItemDelegate(combobox_delegate) projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() From 48e840622dcc24fc8bfb30acadaca25b0ad78477 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Jun 2025 09:54:21 +0200 Subject: [PATCH 079/108] Use `set[str]` for lookup instead of `list[str]` --- .../ayon_core/plugins/publish/extract_color_transcode.py | 2 +- client/ayon_core/plugins/publish/extract_review.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 1e86b91484..8a276cf608 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -58,7 +58,7 @@ class ExtractOIIOTranscode(publish.Extractor): optional = True # Supported extensions - supported_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + supported_exts = {"exr", "jpg", "jpeg", "png", "dpx"} # Configurable by Settings profiles = None diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 3fc2185d1a..13b1e920ef 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -135,11 +135,11 @@ class ExtractReview(pyblish.api.InstancePlugin): ] # Supported extensions - image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"] - video_exts = ["mov", "mp4"] - supported_exts = image_exts + video_exts + image_exts = {"exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"} + video_exts = {"mov", "mp4"} + supported_exts = image_exts.union(video_exts) - alpha_exts = ["exr", "png", "dpx"] + alpha_exts = {"exr", "png", "dpx"} # Preset attributes profiles = [] From 1d55f2a033248c845f2f7ac1432acd85ce49248c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Jun 2025 09:59:09 +0200 Subject: [PATCH 080/108] Update client/ayon_core/pipeline/load/plugins.py --- client/ayon_core/pipeline/load/plugins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 4f0071b4d3..6a6c6c639f 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -300,7 +300,6 @@ class LoaderHookPlugin: container: dict, # (ayon:container-3.0) context: dict, plugin: LoaderPlugin | None = None, - ): pass From 628c8025d4d7964d47c747be263e4b48c6d98540 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 18 Jun 2025 11:48:27 +0200 Subject: [PATCH 081/108] Update client/ayon_core/plugins/publish/extract_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 13b1e920ef..89bc56c670 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -137,7 +137,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # Supported extensions image_exts = {"exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"} video_exts = {"mov", "mp4"} - supported_exts = image_exts.union(video_exts) + supported_exts = image_exts | video_exts alpha_exts = {"exr", "png", "dpx"} From 65b9107d0e5275f1c331f887a90007b10d80a5c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:33:20 +0200 Subject: [PATCH 082/108] return studio settings for empty project --- client/ayon_core/tools/launcher/control.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/launcher/control.py b/client/ayon_core/tools/launcher/control.py index fb9f950bd1..58d22453be 100644 --- a/client/ayon_core/tools/launcher/control.py +++ b/client/ayon_core/tools/launcher/control.py @@ -1,6 +1,6 @@ from ayon_core.lib import Logger, get_ayon_username from ayon_core.lib.events import QueuedEventSystem -from ayon_core.settings import get_project_settings +from ayon_core.settings import get_project_settings, get_studio_settings from ayon_core.tools.common_models import ProjectsModel, HierarchyModel from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend @@ -85,7 +85,10 @@ class BaseLauncherController( def get_project_settings(self, project_name): if project_name in self._project_settings: return self._project_settings[project_name] - settings = get_project_settings(project_name) + if project_name: + settings = get_project_settings(project_name) + else: + settings = get_studio_settings() self._project_settings[project_name] = settings return settings From c7131de67b51884d5fe1a49c31b58410d29d741e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:41:39 +0200 Subject: [PATCH 083/108] implemented deselectable list view --- client/ayon_core/tools/utils/__init__.py | 2 + client/ayon_core/tools/utils/views.py | 58 ++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/utils/__init__.py b/client/ayon_core/tools/utils/__init__.py index 8688430c71..c71c4b71dd 100644 --- a/client/ayon_core/tools/utils/__init__.py +++ b/client/ayon_core/tools/utils/__init__.py @@ -29,6 +29,7 @@ from .widgets import ( from .views import ( DeselectableTreeView, TreeView, + ListView, ) from .error_dialog import ErrorMessageBox from .lib import ( @@ -114,6 +115,7 @@ __all__ = ( "DeselectableTreeView", "TreeView", + "ListView", "ErrorMessageBox", diff --git a/client/ayon_core/tools/utils/views.py b/client/ayon_core/tools/utils/views.py index d69be9b6a9..2ad1d6c7b5 100644 --- a/client/ayon_core/tools/utils/views.py +++ b/client/ayon_core/tools/utils/views.py @@ -37,7 +37,7 @@ class TreeView(QtWidgets.QTreeView): double_clicked = QtCore.Signal(QtGui.QMouseEvent) def __init__(self, *args, **kwargs): - super(TreeView, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._deselectable = False self._flick_charm_activated = False @@ -60,12 +60,64 @@ class TreeView(QtWidgets.QTreeView): self.clearSelection() # clear the current index self.setCurrentIndex(QtCore.QModelIndex()) - super(TreeView, self).mousePressEvent(event) + super().mousePressEvent(event) def mouseDoubleClickEvent(self, event): self.double_clicked.emit(event) - return super(TreeView, self).mouseDoubleClickEvent(event) + return super().mouseDoubleClickEvent(event) + + def activate_flick_charm(self): + if self._flick_charm_activated: + return + self._flick_charm_activated = True + self._before_flick_scroll_mode = self.verticalScrollMode() + self._flick_charm.activateOn(self) + self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + + def deactivate_flick_charm(self): + if not self._flick_charm_activated: + return + self._flick_charm_activated = False + self._flick_charm.deactivateFrom(self) + if self._before_flick_scroll_mode is not None: + self.setVerticalScrollMode(self._before_flick_scroll_mode) + + +class ListView(QtWidgets.QListView): + """A tree view that deselects on clicking on an empty area in the view""" + double_clicked = QtCore.Signal(QtGui.QMouseEvent) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._deselectable = False + + self._flick_charm_activated = False + self._flick_charm = FlickCharm(parent=self) + self._before_flick_scroll_mode = None + + def is_deselectable(self): + return self._deselectable + + def set_deselectable(self, deselectable): + self._deselectable = deselectable + + deselectable = property(is_deselectable, set_deselectable) + + def mousePressEvent(self, event): + if self._deselectable: + index = self.indexAt(event.pos()) + if not index.isValid(): + # clear the selection + self.clearSelection() + # clear the current index + self.setCurrentIndex(QtCore.QModelIndex()) + super().mousePressEvent(event) + + def mouseDoubleClickEvent(self, event): + self.double_clicked.emit(event) + + return super().mouseDoubleClickEvent(event) def activate_flick_charm(self): if self._flick_charm_activated: From 7e29ac837767cbf0264946c19c4cc999824e891e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:42:16 +0200 Subject: [PATCH 084/108] implemented projects widget showing projects as list view --- client/ayon_core/tools/utils/__init__.py | 2 + .../ayon_core/tools/utils/projects_widget.py | 122 ++++++++++++++++-- 2 files changed, 114 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/tools/utils/__init__.py b/client/ayon_core/tools/utils/__init__.py index c71c4b71dd..111b7c614b 100644 --- a/client/ayon_core/tools/utils/__init__.py +++ b/client/ayon_core/tools/utils/__init__.py @@ -62,6 +62,7 @@ from .dialogs import ( ) from .projects_widget import ( ProjectsCombobox, + ProjectsWidget, ProjectsQtModel, ProjectSortFilterProxy, PROJECT_NAME_ROLE, @@ -147,6 +148,7 @@ __all__ = ( "PopupUpdateKeys", "ProjectsCombobox", + "ProjectsWidget", "ProjectsQtModel", "ProjectSortFilterProxy", "PROJECT_NAME_ROLE", diff --git a/client/ayon_core/tools/utils/projects_widget.py b/client/ayon_core/tools/utils/projects_widget.py index c3d0e4160a..ddeb381e8d 100644 --- a/client/ayon_core/tools/utils/projects_widget.py +++ b/client/ayon_core/tools/utils/projects_widget.py @@ -11,6 +11,7 @@ from ayon_core.tools.common_models import ( PROJECTS_MODEL_SENDER, ) +from .views import ListView from .lib import RefreshThread, get_qt_icon if typing.TYPE_CHECKING: @@ -328,7 +329,7 @@ class ProjectsQtModel(QtGui.QStandardItemModel): class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): - super(ProjectSortFilterProxy, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._filter_inactive = True self._filter_standard = False self._filter_library = False @@ -614,9 +615,10 @@ class ProjectsDelegate(QtWidgets.QStyledItemDelegate): }) return self._pin_icon + class ProjectsCombobox(QtWidgets.QWidget): refreshed = QtCore.Signal() - selection_changed = QtCore.Signal() + selection_changed = QtCore.Signal(str) def __init__( self, @@ -672,7 +674,7 @@ class ProjectsCombobox(QtWidgets.QWidget): def refresh(self): self._projects_model.refresh() - def set_selection(self, project_name): + def set_selection(self, project_name: str): """Set selection to a given project. Selection change is ignored if project is not found. @@ -684,8 +686,8 @@ class ProjectsCombobox(QtWidgets.QWidget): bool: True if selection was changed, False otherwise. NOTE: Selection may not be changed if project is not found, or if project is already selected. - """ + """ idx = self._projects_combobox.findData( project_name, PROJECT_NAME_ROLE) if idx < 0: @@ -695,7 +697,7 @@ class ProjectsCombobox(QtWidgets.QWidget): return True return False - def set_listen_to_selection_change(self, listen): + def set_listen_to_selection_change(self, listen: bool): """Disable listening to changes of the selection. Because combobox is triggering selection change when it's model @@ -721,11 +723,11 @@ class ProjectsCombobox(QtWidgets.QWidget): return None return self._projects_combobox.itemData(idx, PROJECT_NAME_ROLE) - def set_current_context_project(self, project_name): + def set_current_context_project(self, project_name: str): self._projects_model.set_current_context_project(project_name) self._projects_proxy_model.invalidateFilter() - def set_select_item_visible(self, visible): + def set_select_item_visible(self, visible: bool): self._select_item_visible = visible self._projects_model.set_select_item_visible(visible) self._update_select_item_visiblity() @@ -763,7 +765,7 @@ class ProjectsCombobox(QtWidgets.QWidget): idx, PROJECT_NAME_ROLE) self._update_select_item_visiblity(project_name=project_name) self._controller.set_selected_project(project_name) - self.selection_changed.emit() + self.selection_changed.emit(project_name or "") def _on_model_refresh(self): self._projects_proxy_model.sort(0) @@ -818,5 +820,105 @@ class ProjectsCombobox(QtWidgets.QWidget): class ProjectsWidget(QtWidgets.QWidget): - # TODO implement - pass + """Projects widget showing projects in list. + + Warnings: + This widget does not support expected selection handling. + + """ + refreshed = QtCore.Signal() + selection_changed = QtCore.Signal(str) + + def __init__( + self, + controller: AbstractProjectController, + parent: Optional[QtWidgets.QWidget] = None + ): + super().__init__(parent=parent) + + projects_view = ListView(parent=self) + projects_view.setResizeMode(QtWidgets.QListView.Adjust) + projects_view.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollPerPixel + ) + projects_view.setAlternatingRowColors(False) + projects_view.setWrapping(False) + projects_view.setWordWrap(False) + projects_view.setSpacing(0) + projects_delegate = ProjectsDelegate(projects_view) + projects_view.setItemDelegate(projects_delegate) + projects_view.activate_flick_charm() + projects_view.set_deselectable(True) + + projects_model = ProjectsQtModel(controller) + projects_proxy_model = ProjectSortFilterProxy() + projects_proxy_model.setSourceModel(projects_model) + projects_view.setModel(projects_proxy_model) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(projects_view, 1) + + projects_view.selectionModel().selectionChanged.connect( + self._on_selection_change + ) + projects_model.refreshed.connect(self._on_model_refresh) + + controller.register_event_callback( + "projects.refresh.finished", + self._on_projects_refresh_finished + ) + + self._controller = controller + + self._projects_view = projects_view + self._projects_model = projects_model + self._projects_proxy_model = projects_proxy_model + self._projects_delegate = projects_delegate + + def has_content(self) -> bool: + """Model has at least one project. + + Returns: + bool: True if there is any content in the model. + + """ + return self._projects_model.has_content() + + def set_name_filter(self, text: str): + self._projects_proxy_model.setFilterFixedString(text) + + def set_selected_project(self, project_name: Optional[str]): + selection_model = self._projects_view.selectionModel() + if project_name is None: + selection_model.clearSelection() + return + index = self._projects_model.get_index_by_project_name(project_name) + if not index.isValid(): + return + proxy_index = self._projects_proxy_model.mapFromSource(index) + if proxy_index.isValid(): + selection_model.select( + proxy_index, + QtCore.QItemSelectionModel.ClearAndSelect + ) + + def _on_model_refresh(self): + self._projects_proxy_model.sort(0) + self._projects_proxy_model.invalidateFilter() + self.refreshed.emit() + + def _on_selection_change(self, new_selection, _old_selection): + project_name = None + for index in new_selection.indexes(): + name = index.data(PROJECT_NAME_ROLE) + if name: + project_name = name + break + self.selection_changed.emit(project_name or "") + self._controller.set_selected_project(project_name) + + def _on_projects_refresh_finished(self, event): + if event["sender"] != PROJECTS_MODEL_SENDER: + self._projects_model.refresh() + From 53803f040dac4d916569d915bc53ced5cdd31869 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:44:04 +0200 Subject: [PATCH 085/108] launcher is using projects widget from utils --- .../tools/launcher/ui/projects_widget.py | 154 ------------------ client/ayon_core/tools/launcher/ui/window.py | 46 +++++- 2 files changed, 41 insertions(+), 159 deletions(-) delete mode 100644 client/ayon_core/tools/launcher/ui/projects_widget.py diff --git a/client/ayon_core/tools/launcher/ui/projects_widget.py b/client/ayon_core/tools/launcher/ui/projects_widget.py deleted file mode 100644 index e2af54b55d..0000000000 --- a/client/ayon_core/tools/launcher/ui/projects_widget.py +++ /dev/null @@ -1,154 +0,0 @@ -from qtpy import QtWidgets, QtCore - -from ayon_core.tools.flickcharm import FlickCharm -from ayon_core.tools.utils import ( - PlaceholderLineEdit, - RefreshButton, - ProjectsQtModel, - ProjectSortFilterProxy, -) -from ayon_core.tools.common_models import PROJECTS_MODEL_SENDER - - -class ProjectIconView(QtWidgets.QListView): - """Styled ListView that allows to toggle between icon and list mode. - - Toggling between the two modes is done by Right Mouse Click. - """ - - IconMode = 0 - ListMode = 1 - - def __init__(self, parent=None, mode=ListMode): - super(ProjectIconView, self).__init__(parent=parent) - - # Workaround for scrolling being super slow or fast when - # toggling between the two visual modes - self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - self.setObjectName("IconView") - - self._mode = None - self.set_mode(mode) - - def set_mode(self, mode): - if mode == self._mode: - return - - self._mode = mode - - if mode == self.IconMode: - self.setViewMode(QtWidgets.QListView.IconMode) - self.setResizeMode(QtWidgets.QListView.Adjust) - self.setWrapping(True) - self.setWordWrap(True) - self.setGridSize(QtCore.QSize(151, 90)) - self.setIconSize(QtCore.QSize(50, 50)) - self.setSpacing(0) - self.setAlternatingRowColors(False) - - self.setProperty("mode", "icon") - self.style().polish(self) - - self.verticalScrollBar().setSingleStep(30) - - elif self.ListMode: - self.setProperty("mode", "list") - self.style().polish(self) - - self.setViewMode(QtWidgets.QListView.ListMode) - self.setResizeMode(QtWidgets.QListView.Adjust) - self.setWrapping(False) - self.setWordWrap(False) - self.setIconSize(QtCore.QSize(20, 20)) - self.setGridSize(QtCore.QSize(100, 25)) - self.setSpacing(0) - self.setAlternatingRowColors(False) - - self.verticalScrollBar().setSingleStep(34) - - def mousePressEvent(self, event): - if event.button() == QtCore.Qt.RightButton: - self.set_mode(int(not self._mode)) - return super(ProjectIconView, self).mousePressEvent(event) - - -class ProjectsWidget(QtWidgets.QWidget): - """Projects Page""" - - refreshed = QtCore.Signal() - - def __init__(self, controller, parent=None): - super(ProjectsWidget, self).__init__(parent=parent) - - header_widget = QtWidgets.QWidget(self) - - projects_filter_text = PlaceholderLineEdit(header_widget) - projects_filter_text.setPlaceholderText("Filter projects...") - - refresh_btn = RefreshButton(header_widget) - - header_layout = QtWidgets.QHBoxLayout(header_widget) - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.addWidget(projects_filter_text, 1) - header_layout.addWidget(refresh_btn, 0) - - projects_view = ProjectIconView(parent=self) - projects_view.setSelectionMode(QtWidgets.QListView.NoSelection) - flick = FlickCharm(parent=self) - flick.activateOn(projects_view) - projects_model = ProjectsQtModel(controller) - projects_proxy_model = ProjectSortFilterProxy() - projects_proxy_model.setSourceModel(projects_model) - - projects_view.setModel(projects_proxy_model) - - main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.addWidget(header_widget, 0) - main_layout.addWidget(projects_view, 1) - - projects_view.clicked.connect(self._on_view_clicked) - projects_model.refreshed.connect(self.refreshed) - projects_filter_text.textChanged.connect( - self._on_project_filter_change) - refresh_btn.clicked.connect(self._on_refresh_clicked) - - controller.register_event_callback( - "projects.refresh.finished", - self._on_projects_refresh_finished - ) - - self._controller = controller - - self._projects_view = projects_view - self._projects_model = projects_model - self._projects_proxy_model = projects_proxy_model - - def has_content(self): - """Model has at least one project. - - Returns: - bool: True if there is any content in the model. - """ - - return self._projects_model.has_content() - - def _on_view_clicked(self, index): - if not index.isValid(): - return - model = index.model() - flags = model.flags(index) - if not flags & QtCore.Qt.ItemIsEnabled: - return - project_name = index.data(QtCore.Qt.DisplayRole) - self._controller.set_selected_project(project_name) - - def _on_project_filter_change(self, text): - self._projects_proxy_model.setFilterFixedString(text) - - def _on_refresh_clicked(self): - self._controller.refresh() - - def _on_projects_refresh_finished(self, event): - if event["sender"] != PROJECTS_MODEL_SENDER: - self._projects_model.refresh() diff --git a/client/ayon_core/tools/launcher/ui/window.py b/client/ayon_core/tools/launcher/ui/window.py index 7fde8518b0..819e141d59 100644 --- a/client/ayon_core/tools/launcher/ui/window.py +++ b/client/ayon_core/tools/launcher/ui/window.py @@ -3,9 +3,13 @@ from qtpy import QtWidgets, QtCore, QtGui from ayon_core import style, resources from ayon_core.tools.launcher.control import BaseLauncherController -from ayon_core.tools.utils import MessageOverlayObject +from ayon_core.tools.utils import ( + MessageOverlayObject, + PlaceholderLineEdit, + RefreshButton, + ProjectsWidget, +) -from .projects_widget import ProjectsWidget from .hierarchy_page import HierarchyPage from .actions_widget import ActionsWidget @@ -50,7 +54,25 @@ class LauncherWindow(QtWidgets.QWidget): pages_widget = QtWidgets.QWidget(content_body) # - First page - Projects - projects_page = ProjectsWidget(controller, pages_widget) + projects_page = QtWidgets.QWidget(pages_widget) + projects_header_widget = QtWidgets.QWidget(projects_page) + + projects_filter_text = PlaceholderLineEdit(projects_header_widget) + projects_filter_text.setPlaceholderText("Filter projects...") + + refresh_btn = RefreshButton(projects_header_widget) + + projects_header_layout = QtWidgets.QHBoxLayout(projects_header_widget) + projects_header_layout.setContentsMargins(0, 0, 0, 0) + projects_header_layout.addWidget(projects_filter_text, 1) + projects_header_layout.addWidget(refresh_btn, 0) + + projects_widget = ProjectsWidget(controller, pages_widget) + + projects_layout = QtWidgets.QVBoxLayout(projects_page) + projects_layout.setContentsMargins(0, 0, 0, 0) + projects_layout.addWidget(projects_header_widget, 0) + projects_layout.addWidget(projects_widget, 1) # - Second page - Hierarchy (folders & tasks) hierarchy_page = HierarchyPage(controller, pages_widget) @@ -102,12 +124,16 @@ class LauncherWindow(QtWidgets.QWidget): page_slide_anim.setEndValue(1.0) page_slide_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad) - projects_page.refreshed.connect(self._on_projects_refresh) + refresh_btn.clicked.connect(self._on_refresh_request) + projects_widget.refreshed.connect(self._on_projects_refresh) + actions_refresh_timer.timeout.connect( self._on_actions_refresh_timeout) page_slide_anim.valueChanged.connect( self._on_page_slide_value_changed) page_slide_anim.finished.connect(self._on_page_slide_finished) + projects_filter_text.textChanged.connect( + self._on_project_filter_change) controller.register_event_callback( "selection.project.changed", @@ -142,6 +168,7 @@ class LauncherWindow(QtWidgets.QWidget): self._pages_widget = pages_widget self._pages_layout = pages_layout self._projects_page = projects_page + self._projects_widget = projects_widget self._hierarchy_page = hierarchy_page self._actions_widget = actions_widget # self._action_history = action_history @@ -194,6 +221,12 @@ class LauncherWindow(QtWidgets.QWidget): elif self._is_on_projects_page: self._go_to_hierarchy_page(project_name) + def _on_project_filter_change(self, text): + self._projects_widget.set_name_filter(text) + + def _on_refresh_request(self): + self._controller.refresh() + def _on_projects_refresh(self): # Refresh only actions on projects page if self._is_on_projects_page: @@ -201,7 +234,7 @@ class LauncherWindow(QtWidgets.QWidget): return # No projects were found -> go back to projects page - if not self._projects_page.has_content(): + if not self._projects_widget.has_content(): self._go_to_projects_page() return @@ -280,6 +313,9 @@ class LauncherWindow(QtWidgets.QWidget): def _go_to_projects_page(self): if self._is_on_projects_page: return + + # Deselect project in projects widget + self._projects_widget.set_selected_project(None) self._is_on_projects_page = True self._hierarchy_page.set_page_visible(False) From 79553d0ce7cb17f2e1acda47ad10ad62ffe4419b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:49:41 +0200 Subject: [PATCH 086/108] minor modifications of selection --- client/ayon_core/tools/utils/projects_widget.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/utils/projects_widget.py b/client/ayon_core/tools/utils/projects_widget.py index ddeb381e8d..d10a68cfeb 100644 --- a/client/ayon_core/tools/utils/projects_widget.py +++ b/client/ayon_core/tools/utils/projects_widget.py @@ -889,15 +889,17 @@ class ProjectsWidget(QtWidgets.QWidget): self._projects_proxy_model.setFilterFixedString(text) def set_selected_project(self, project_name: Optional[str]): - selection_model = self._projects_view.selectionModel() if project_name is None: - selection_model.clearSelection() + self._projects_view.clearSelection() + self._projects_view.setCurrentIndex(QtCore.QModelIndex()) return + index = self._projects_model.get_index_by_project_name(project_name) if not index.isValid(): return proxy_index = self._projects_proxy_model.mapFromSource(index) if proxy_index.isValid(): + selection_model = self._projects_view.selectionModel() selection_model.select( proxy_index, QtCore.QItemSelectionModel.ClearAndSelect From 1d397ec97794ddc273418660418a5130fc39cc27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:59:16 +0200 Subject: [PATCH 087/108] handle text margins --- client/ayon_core/tools/utils/projects_widget.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/tools/utils/projects_widget.py b/client/ayon_core/tools/utils/projects_widget.py index d10a68cfeb..2e3442a3dd 100644 --- a/client/ayon_core/tools/utils/projects_widget.py +++ b/client/ayon_core/tools/utils/projects_widget.py @@ -563,6 +563,12 @@ class ProjectsDelegate(QtWidgets.QStyledItemDelegate): painter.setPen(opt.palette.color(cg, QtGui.QPalette.Text)) painter.drawRect(text_rect.adjusted(0, 0, -1, -1)) + margin = proxy.pixelMetric( + QtWidgets.QStyle.PM_FocusFrameHMargin, None, widget + ) + 1 + text_rect.adjust(margin, 0, -margin, 0) + # NOTE skipping some steps e.g. word wrapping and elided + # text (adding '...' when too long). painter.drawText( text_rect, opt.displayAlignment, From a785ea20e9d310b868391ca2174fb75e4cb9ce69 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:45:19 +0200 Subject: [PATCH 088/108] added more helper functions --- client/ayon_core/tools/utils/projects_widget.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/utils/projects_widget.py b/client/ayon_core/tools/utils/projects_widget.py index 2e3442a3dd..34104ef74a 100644 --- a/client/ayon_core/tools/utils/projects_widget.py +++ b/client/ayon_core/tools/utils/projects_widget.py @@ -419,7 +419,6 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): return r_is_library return super().lessThan(left_index, right_index) - def filterAcceptsRow(self, source_row, source_parent): index = self.sourceModel().index(source_row, 0, source_parent) project_name = index.data(PROJECT_NAME_ROLE) @@ -834,6 +833,7 @@ class ProjectsWidget(QtWidgets.QWidget): """ refreshed = QtCore.Signal() selection_changed = QtCore.Signal(str) + double_clicked = QtCore.Signal() def __init__( self, @@ -868,6 +868,7 @@ class ProjectsWidget(QtWidgets.QWidget): projects_view.selectionModel().selectionChanged.connect( self._on_selection_change ) + projects_view.double_clicked.connect(self.double_clicked) projects_model.refreshed.connect(self._on_model_refresh) controller.register_event_callback( @@ -882,6 +883,9 @@ class ProjectsWidget(QtWidgets.QWidget): self._projects_proxy_model = projects_proxy_model self._projects_delegate = projects_delegate + def refresh(self): + self._projects_model.refresh() + def has_content(self) -> bool: """Model has at least one project. @@ -894,6 +898,14 @@ class ProjectsWidget(QtWidgets.QWidget): def set_name_filter(self, text: str): self._projects_proxy_model.setFilterFixedString(text) + def get_selected_project(self) -> Optional[str]: + selection_model = self._projects_view.selectionModel() + for index in selection_model.selectedIndexes(): + project_name = index.data(PROJECT_NAME_ROLE) + if project_name: + return project_name + return None + def set_selected_project(self, project_name: Optional[str]): if project_name is None: self._projects_view.clearSelection() From 68d9fc16ad278f15022f936a15557f3069aa458d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:47:09 +0200 Subject: [PATCH 089/108] convert pinned projects to pinned set --- client/ayon_core/tools/common_models/projects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/common_models/projects.py b/client/ayon_core/tools/common_models/projects.py index 9bbdc8a75c..f2599c9c9b 100644 --- a/client/ayon_core/tools/common_models/projects.py +++ b/client/ayon_core/tools/common_models/projects.py @@ -441,6 +441,7 @@ class ProjectsModel(object): .get("frontendPreferences", {}) .get("pinnedProjects") ) or [] + pinned_projects = set(pinned_projects) project_items = _get_project_items_from_entitiy(list(projects)) for project in project_items: project.is_pinned = project.name in pinned_projects From 12d25d805c1bb385b7a41074d4109cd6d933cdba Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:56:15 +0200 Subject: [PATCH 090/108] formatting fixes --- client/ayon_core/tools/utils/projects_widget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/utils/projects_widget.py b/client/ayon_core/tools/utils/projects_widget.py index 34104ef74a..1c87d79a58 100644 --- a/client/ayon_core/tools/utils/projects_widget.py +++ b/client/ayon_core/tools/utils/projects_widget.py @@ -22,7 +22,6 @@ if typing.TYPE_CHECKING: current: Optional[str] selected: Optional[str] - class ExpectedSelectionData(TypedDict): project: ExpectedProjectSelectionData @@ -554,7 +553,9 @@ class ProjectsDelegate(QtWidgets.QStyledItemDelegate): cg = QtGui.QPalette.Normal if opt.state & QtWidgets.QStyle.State_Selected: - painter.setPen(opt.palette.color(cg, QtGui.QPalette.HighlightedText)) + painter.setPen( + opt.palette.color(cg, QtGui.QPalette.HighlightedText) + ) else: painter.setPen(opt.palette.color(cg, QtGui.QPalette.Text)) @@ -941,4 +942,3 @@ class ProjectsWidget(QtWidgets.QWidget): def _on_projects_refresh_finished(self, event): if event["sender"] != PROJECTS_MODEL_SENDER: self._projects_model.refresh() - From f4af01f702b7f7fc339f19231e0b88a7ee56fc33 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jun 2025 18:59:39 +0200 Subject: [PATCH 091/108] :burn: remove `TypedDict` to retain compatibility with pythpn 3.7 but we should get it back (or dataclasses) when we get out of Middle Ages. --- client/ayon_core/tools/loader/abstract.py | 169 +++------------------- 1 file changed, 17 insertions(+), 152 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 804956f875..de0a1c7dd8 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import List, Optional, TypedDict +from typing import Any, List, Optional from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -10,62 +10,16 @@ from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, ) -IconData = TypedDict("IconData", { - "type": str, - "name": str, - "color": str -}) - -ProductBaseTypeItemData = TypedDict("ProductBaseTypeItemData", { - "name": str, - "icon": IconData -}) - - -VersionItemData = TypedDict("VersionItemData", { - "version_id": str, - "version": int, - "is_hero": bool, - "product_id": str, - "task_id": Optional[str], - "thumbnail_id": Optional[str], - "published_time": Optional[str], - "author": Optional[str], - "status": Optional[str], - "frame_range": Optional[str], - "duration": Optional[int], - "handles": Optional[str], - "step": Optional[int], - "comment": Optional[str], - "source": Optional[str] -}) - - -ProductItemData = TypedDict("ProductItemData", { - "product_id": str, - "product_type": str, - "product_base_type": str, - "product_name": str, - "product_icon": IconData, - "product_type_icon": IconData, - "product_base_type_icon": IconData, - "group_name": str, - "folder_id": str, - "folder_label": str, - "version_items": dict[str, VersionItemData], - "product_in_scene": bool -}) - class ProductTypeItem: """Item representing product type. Args: name (str): Product type name. - icon (IconData): Product type icon definition. + icon (dict[str, str]): Product type icon definition. """ - def __init__(self, name: str, icon: IconData): + def __init__(self, name: str, icon: dict[str, str]): self.name = name self.icon = icon @@ -83,16 +37,16 @@ class ProductTypeItem: class ProductBaseTypeItem: """Item representing the product base type.""" - def __init__(self, name: str, icon: IconData): + def __init__(self, name: str, icon: dict[str, str]): """Initialize product base type item.""" self.name = name self.icon = icon - def to_data(self) -> ProductBaseTypeItemData: + def to_data(self) -> dict[str, Any]: """Convert item to data dictionary. Returns: - ProductBaseTypeItemData: Data representation of the item. + dict[str, Any]: Data representation of the item. """ return { @@ -102,11 +56,11 @@ class ProductBaseTypeItem: @classmethod def from_data( - cls, data: ProductBaseTypeItemData) -> ProductBaseTypeItem: + cls, data: dict[str, Any]) -> ProductBaseTypeItem: """Create item from data dictionary. Args: - data (ProductBaseTypeItemData): Data to create item from. + data (dict[str, Any]): Data to create item from. Returns: ProductBaseTypeItem: Item created from the provided data. @@ -122,8 +76,8 @@ class ProductItem: product_id (str): Product id. product_type (str): Product type. product_name (str): Product name. - product_icon (IconData): Product icon definition. - product_type_icon (IconData): Product type icon definition. + product_icon (dict[str, str]): Product icon definition. + product_type_icon (dict[str, str]): Product type icon definition. product_in_scene (bool): Is product in scene (only when used in DCC). group_name (str): Group name. folder_id (str): Folder id. @@ -137,9 +91,9 @@ class ProductItem: product_type: str, product_base_type: str, product_name: str, - product_icon: IconData, - product_type_icon: IconData, - product_base_type_icon: IconData, + product_icon: dict[str, str], + product_type_icon: dict[str, str], + product_base_type_icon: dict[str, str], group_name: str, folder_id: str, folder_label: str, @@ -159,7 +113,7 @@ class ProductItem: self.folder_label = folder_label self.version_items = version_items - def to_data(self) -> ProductItemData: + def to_data(self) -> dict[str, Any]: return { "product_id": self.product_id, "product_type": self.product_type, @@ -408,10 +362,8 @@ class ActionItem: # future development of detached UI tools it would be better to be # prepared for it. raise NotImplementedError( - "{}.to_data is not implemented. Use Attribute definitions" - " from 'ayon_core.lib' instead of 'qargparse'.".format( - self.__class__.__name__ - ) + f"{self.__class__.__name__}.to_data is not implemented. Use Attribute definitions" + " from 'ayon_core.lib' instead of 'qargparse'." ) def to_data(self): @@ -470,8 +422,6 @@ class _BaseLoaderController(ABC): dict[str, Union[str, None]]: Context data. """ - pass - @abstractmethod def reset(self): """Reset all cached data to reload everything. @@ -480,8 +430,6 @@ class _BaseLoaderController(ABC): "controller.reset.finished". """ - pass - # Model wrappers @abstractmethod def get_folder_items(self, project_name, sender=None): @@ -495,8 +443,6 @@ class _BaseLoaderController(ABC): list[FolderItem]: Folder items for the project. """ - pass - # Expected selection helpers @abstractmethod def get_expected_selection_data(self): @@ -510,8 +456,6 @@ class _BaseLoaderController(ABC): dict[str, Any]: Expected selection data. """ - pass - @abstractmethod def set_expected_selection(self, project_name, folder_id): """Set expected selection. @@ -521,8 +465,6 @@ class _BaseLoaderController(ABC): folder_id (str): Id of folder to be selected. """ - pass - class BackendLoaderController(_BaseLoaderController): """Backend loader controller abstraction. @@ -542,8 +484,6 @@ class BackendLoaderController(_BaseLoaderController): source (Optional[str]): Event source. """ - pass - @abstractmethod def get_loaded_product_ids(self): """Return set of loaded product ids. @@ -552,8 +492,6 @@ class BackendLoaderController(_BaseLoaderController): set[str]: Set of loaded product ids. """ - pass - class FrontendLoaderController(_BaseLoaderController): @abstractmethod @@ -565,8 +503,6 @@ class FrontendLoaderController(_BaseLoaderController): callback (func): Callback triggered when the event is emitted. """ - pass - # Expected selection helpers @abstractmethod def expected_project_selected(self, project_name): @@ -576,8 +512,6 @@ class FrontendLoaderController(_BaseLoaderController): project_name (str): Project name. """ - pass - @abstractmethod def expected_folder_selected(self, folder_id): """Expected folder was selected in frontend. @@ -586,8 +520,6 @@ class FrontendLoaderController(_BaseLoaderController): folder_id (str): Folder id. """ - pass - # Model wrapper calls @abstractmethod def get_project_items(self, sender=None): @@ -609,8 +541,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ProjectItem]: List of project items. """ - pass - @abstractmethod def get_folder_type_items(self, project_name, sender=None): """Folder type items for a project. @@ -629,7 +559,6 @@ class FrontendLoaderController(_BaseLoaderController): list[FolderTypeItem]: Folder type information. """ - pass @abstractmethod def get_task_items(self, project_name, folder_ids, sender=None): @@ -644,7 +573,6 @@ class FrontendLoaderController(_BaseLoaderController): list[TaskItem]: List of task items. """ - pass @abstractmethod def get_task_type_items(self, project_name, sender=None): @@ -664,7 +592,6 @@ class FrontendLoaderController(_BaseLoaderController): list[TaskTypeItem]: Task type information. """ - pass @abstractmethod def get_folder_labels(self, project_name, folder_ids): @@ -678,7 +605,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Optional[str]]: Folder labels by folder id. """ - pass @abstractmethod def get_project_status_items(self, project_name, sender=None): @@ -699,8 +625,6 @@ class FrontendLoaderController(_BaseLoaderController): list[StatusItem]: List of status items. """ - pass - @abstractmethod def get_product_items(self, project_name, folder_ids, sender=None): """Product items for folder ids. @@ -722,8 +646,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ProductItem]: List of product items. """ - pass - @abstractmethod def get_product_item(self, project_name, product_id): """Receive single product item. @@ -736,8 +658,6 @@ class FrontendLoaderController(_BaseLoaderController): Union[ProductItem, None]: Product info or None if not found. """ - pass - @abstractmethod def get_product_type_items(self, project_name): """Product type items for a project. @@ -751,8 +671,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ProductTypeItem]: List of product type items for a project. """ - pass - @abstractmethod def get_representation_items( self, project_name, version_ids, sender=None @@ -776,8 +694,6 @@ class FrontendLoaderController(_BaseLoaderController): list[RepreItem]: List of representation items. """ - pass - @abstractmethod def get_version_thumbnail_ids(self, project_name, version_ids): """Get thumbnail ids for version ids. @@ -790,8 +706,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, Any]]: Thumbnail id by version id. """ - pass - @abstractmethod def get_folder_thumbnail_ids(self, project_name, folder_ids): """Get thumbnail ids for folder ids. @@ -804,14 +718,11 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, Any]]: Thumbnail id by folder id. """ - pass - @abstractmethod def get_versions_representation_count( self, project_name, version_ids, sender=None ): - """ - Args: + """Args: project_name (str): Project name. version_ids (Iterable[str]): Version ids. sender (Optional[str]): Sender who requested the items. @@ -820,8 +731,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, int]: Representation count by version id. """ - pass - @abstractmethod def get_thumbnail_paths( self, @@ -844,8 +753,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, None]]: Thumbnail path by entity id. """ - pass - # Selection model wrapper calls @abstractmethod def get_selected_project_name(self): @@ -857,8 +764,6 @@ class FrontendLoaderController(_BaseLoaderController): Union[str, None]: Selected project name. """ - pass - @abstractmethod def get_selected_folder_ids(self): """Get selected folder ids. @@ -869,7 +774,6 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected folder ids. """ - pass @abstractmethod def get_selected_task_ids(self): @@ -881,7 +785,6 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected folder ids. """ - pass @abstractmethod def set_selected_tasks(self, task_ids): @@ -891,7 +794,6 @@ class FrontendLoaderController(_BaseLoaderController): task_ids (Iterable[str]): Selected task ids. """ - pass @abstractmethod def get_selected_version_ids(self): @@ -903,7 +805,6 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected version ids. """ - pass @abstractmethod def get_selected_representation_ids(self): @@ -915,8 +816,6 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected representation ids. """ - pass - @abstractmethod def set_selected_project(self, project_name): """Set selected project. @@ -931,8 +830,6 @@ class FrontendLoaderController(_BaseLoaderController): project_name (Union[str, None]): Selected project name. """ - pass - @abstractmethod def set_selected_folders(self, folder_ids): """Set selected folders. @@ -948,8 +845,6 @@ class FrontendLoaderController(_BaseLoaderController): folder_ids (Iterable[str]): Selected folder ids. """ - pass - @abstractmethod def set_selected_versions(self, version_ids): """Set selected versions. @@ -966,8 +861,6 @@ class FrontendLoaderController(_BaseLoaderController): version_ids (Iterable[str]): Selected version ids. """ - pass - @abstractmethod def set_selected_representations(self, repre_ids): """Set selected representations. @@ -985,8 +878,6 @@ class FrontendLoaderController(_BaseLoaderController): repre_ids (Iterable[str]): Selected representation ids. """ - pass - # Load action items @abstractmethod def get_versions_action_items(self, project_name, version_ids): @@ -1000,8 +891,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ActionItem]: List of action items. """ - pass - @abstractmethod def get_representations_action_items( self, project_name, representation_ids @@ -1016,8 +905,6 @@ class FrontendLoaderController(_BaseLoaderController): list[ActionItem]: List of action items. """ - pass - @abstractmethod def trigger_action_item( self, @@ -1050,8 +937,6 @@ class FrontendLoaderController(_BaseLoaderController): representation_ids (Iterable[str]): Representation ids. """ - pass - @abstractmethod def change_products_group(self, project_name, product_ids, group_name): """Change group of products. @@ -1070,8 +955,6 @@ class FrontendLoaderController(_BaseLoaderController): group_name (str): New group name. """ - pass - @abstractmethod def fill_root_in_source(self, source): """Fill root in source path. @@ -1081,8 +964,6 @@ class FrontendLoaderController(_BaseLoaderController): rootless workfile path. """ - pass - # NOTE: Methods 'is_loaded_products_supported' and # 'is_standard_projects_filter_enabled' are both based on being in host # or not. Maybe we could implement only single method 'is_in_host'? @@ -1094,8 +975,6 @@ class FrontendLoaderController(_BaseLoaderController): bool: True if it is supported. """ - pass - @abstractmethod def is_standard_projects_filter_enabled(self): """Is standard projects filter enabled. @@ -1108,8 +987,6 @@ class FrontendLoaderController(_BaseLoaderController): current context project. """ - pass - # Site sync functions @abstractmethod def is_sitesync_enabled(self, project_name=None): @@ -1127,8 +1004,6 @@ class FrontendLoaderController(_BaseLoaderController): bool: True if site sync is enabled. """ - pass - @abstractmethod def get_active_site_icon_def(self, project_name): """Active site icon definition. @@ -1141,8 +1016,6 @@ class FrontendLoaderController(_BaseLoaderController): is not enabled for the project. """ - pass - @abstractmethod def get_remote_site_icon_def(self, project_name): """Remote site icon definition. @@ -1155,8 +1028,6 @@ class FrontendLoaderController(_BaseLoaderController): is not enabled for the project. """ - pass - @abstractmethod def get_version_sync_availability(self, project_name, version_ids): """Version sync availability. @@ -1169,8 +1040,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, tuple[int, int]]: Sync availability by version id. """ - pass - @abstractmethod def get_representations_sync_status( self, project_name, representation_ids @@ -1185,8 +1054,6 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, tuple[int, int]]: Sync status by representation id. """ - pass - @abstractmethod def get_product_types_filter(self): """Return product type filter for current context. @@ -1194,5 +1061,3 @@ class FrontendLoaderController(_BaseLoaderController): Returns: ProductTypesFilter: Product type filter for current context """ - - pass From ae1bfc71f78bd43a0e930387561178c799184c43 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 18 Jun 2025 19:00:35 +0200 Subject: [PATCH 092/108] :recycle: change loader filtering --- client/ayon_core/pipeline/load/plugins.py | 81 ++++++++++++++--------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 32b96e3e7a..5f206df364 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,16 +1,18 @@ """Plugins for loading representations and products into host applications.""" from __future__ import annotations -import os -import logging -from ayon_core.settings import get_project_settings +import logging +import os + from ayon_core.pipeline.plugin_discover import ( + deregister_plugin, + deregister_plugin_path, discover, register_plugin, register_plugin_path, - deregister_plugin, - deregister_plugin_path ) +from ayon_core.settings import get_project_settings + from .utils import get_representation_path_from_context @@ -61,12 +63,12 @@ class LoaderPlugin(list): if not plugin_settings: return - print(">>> We have preset for {}".format(plugin_name)) + print(f">>> We have preset for {plugin_name}") for option, value in plugin_settings.items(): if option == "enabled" and value is False: print(" - is disabled by preset") else: - print(" - setting `{}`: `{}`".format(option, value)) + print(f" - setting `{option}`: `{value}`") setattr(cls, option, value) @classmethod @@ -79,7 +81,6 @@ class LoaderPlugin(list): Returns: bool: Representation has valid extension """ - if "*" in cls.extensions: return True @@ -122,10 +123,21 @@ class LoaderPlugin(list): Returns: bool: Is loader compatible for context. """ + """ + product_types: set[str] = set() + product_base_types: set[str] = set() + representations = set() + extensions = {"*"} + """ plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types plugin_product_base_types = cls.product_base_types + repre_entity = context.get("representation") + product_entity = context["product"] + + # If no representation names, product types or extensions are defined + # then loader is not compatible with any context. if ( not plugin_repre_names or (not plugin_product_types and not plugin_product_base_types) @@ -133,38 +145,45 @@ class LoaderPlugin(list): ): return False - repre_entity = context.get("representation") + # If no representation entity is provided then loader is not + # compatible with context. if not repre_entity: return False + # Check the compatibility with the representation names. plugin_repre_names = set(plugin_repre_names) - if ( + if not plugin_repre_names or ( "*" not in plugin_repre_names and repre_entity["name"] not in plugin_repre_names ): return False + # Check the compatibility with the extension of the representation. if not cls.has_valid_extension(repre_entity): return False plugin_product_types = set(plugin_product_types) - if "*" in plugin_product_types: - return True - - plugin_product_base_types = set(plugin_product_base_types) - if "*" in plugin_product_base_types: - # If plugin supports all product base types, then it is compatible - # with any product type. - return True - - product_entity = context["product"] - product_type = product_entity["productType"] + product_type = product_entity.get("productType") product_base_type = product_entity.get("productBaseType") - if product_type in plugin_product_types: + # Use product base type if defined, otherwise use product type. + product_filter = product_base_type + # If there is no product base type defined in the product entity, + # then we will use the product type. + if product_filter is None: + product_filter = product_type + + # If no product type isn't defined on the loader plugin, + # then we will use the product types. + plugin_product_filter = ( + plugin_product_base_types or plugin_product_types) + + # If wildcard is used in product types or base types, + # then we will consider the loader compatible with any product type. + if "*" in plugin_product_filter: return True - return product_base_type in plugin_product_base_types + return product_filter in plugin_product_filter @classmethod def get_representations(cls): @@ -219,19 +238,17 @@ class LoaderPlugin(list): bool: Whether the container was deleted """ - raise NotImplementedError("Loader.remove() must be " "implemented by subclass") @classmethod def get_options(cls, contexts): - """ - Returns static (cls) options or could collect from 'contexts'. + """Returns static (cls) options or could collect from 'contexts'. - Args: - contexts (list): of repre or product contexts - Returns: - (list) + Args: + contexts (list): of repre or product contexts + Returns: + (list) """ return cls.options or [] @@ -279,9 +296,7 @@ def discover_loader_plugins(project_name=None): plugin.apply_settings(project_settings) except Exception: log.warning( - "Failed to apply settings to loader {}".format( - plugin.__name__ - ), + f"Failed to apply settings to loader {plugin.__name__}", exc_info=True ) return plugins From 1c63b75a272421e2f0ebdd88df791802b9097204 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 Jun 2025 10:06:29 +0200 Subject: [PATCH 093/108] :recycle: make product type and product base types None by default --- client/ayon_core/pipeline/load/plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 5f206df364..966b418db8 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import os +from typing import Optional from ayon_core.pipeline.plugin_discover import ( deregister_plugin, @@ -19,8 +20,8 @@ from .utils import get_representation_path_from_context class LoaderPlugin(list): """Load representation into host application""" - product_types: set[str] = set() - product_base_types: set[str] = set() + product_types: Optional[set[str]] = None + product_base_types: Optional[set[str]] = None representations = set() extensions = {"*"} order = 0 From a3c04d232a6c91ed303ca40f1b4386601be0d21c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 Jun 2025 10:07:20 +0200 Subject: [PATCH 094/108] :recycle: revert more `TypedDict` changes and fix line length --- client/ayon_core/tools/loader/abstract.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index de0a1c7dd8..c585160672 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -242,7 +242,7 @@ class VersionItem: def __le__(self, other): return self.__eq__(other) or self.__lt__(other) - def to_data(self) -> VersionItemData: + def to_data(self) -> dict[str, Any]: return { "version_id": self.version_id, "product_id": self.product_id, @@ -262,7 +262,7 @@ class VersionItem: } @classmethod - def from_data(cls, data: VersionItemData): + def from_data(cls, data: dict[str, Any]) -> VersionItem: return cls(**data) @@ -362,8 +362,9 @@ class ActionItem: # future development of detached UI tools it would be better to be # prepared for it. raise NotImplementedError( - f"{self.__class__.__name__}.to_data is not implemented. Use Attribute definitions" - " from 'ayon_core.lib' instead of 'qargparse'." + f"{self.__class__.__name__}.to_data is not implemented. " + "Use Attribute definitions " + "from 'ayon_core.lib' instead of 'qargparse'." ) def to_data(self): From e003ef2960727bdc997da5a737f3458786237af0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 19 Jun 2025 10:15:02 +0200 Subject: [PATCH 095/108] :fire: revert some code cleanup --- client/ayon_core/tools/loader/abstract.py | 96 ++++++++++++++++++++++- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index c585160672..21f2349544 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -362,9 +362,10 @@ class ActionItem: # future development of detached UI tools it would be better to be # prepared for it. raise NotImplementedError( - f"{self.__class__.__name__}.to_data is not implemented. " - "Use Attribute definitions " - "from 'ayon_core.lib' instead of 'qargparse'." + "{}.to_data is not implemented. Use Attribute definitions" + " from 'ayon_core.lib' instead of 'qargparse'.".format( + self.__class__.__name__ + ) ) def to_data(self): @@ -423,6 +424,8 @@ class _BaseLoaderController(ABC): dict[str, Union[str, None]]: Context data. """ + pass + @abstractmethod def reset(self): """Reset all cached data to reload everything. @@ -431,6 +434,8 @@ class _BaseLoaderController(ABC): "controller.reset.finished". """ + pass + # Model wrappers @abstractmethod def get_folder_items(self, project_name, sender=None): @@ -444,6 +449,8 @@ class _BaseLoaderController(ABC): list[FolderItem]: Folder items for the project. """ + pass + # Expected selection helpers @abstractmethod def get_expected_selection_data(self): @@ -457,6 +464,8 @@ class _BaseLoaderController(ABC): dict[str, Any]: Expected selection data. """ + pass + @abstractmethod def set_expected_selection(self, project_name, folder_id): """Set expected selection. @@ -466,6 +475,8 @@ class _BaseLoaderController(ABC): folder_id (str): Id of folder to be selected. """ + pass + class BackendLoaderController(_BaseLoaderController): """Backend loader controller abstraction. @@ -485,6 +496,8 @@ class BackendLoaderController(_BaseLoaderController): source (Optional[str]): Event source. """ + pass + @abstractmethod def get_loaded_product_ids(self): """Return set of loaded product ids. @@ -493,6 +506,8 @@ class BackendLoaderController(_BaseLoaderController): set[str]: Set of loaded product ids. """ + pass + class FrontendLoaderController(_BaseLoaderController): @abstractmethod @@ -504,6 +519,8 @@ class FrontendLoaderController(_BaseLoaderController): callback (func): Callback triggered when the event is emitted. """ + pass + # Expected selection helpers @abstractmethod def expected_project_selected(self, project_name): @@ -513,6 +530,8 @@ class FrontendLoaderController(_BaseLoaderController): project_name (str): Project name. """ + pass + @abstractmethod def expected_folder_selected(self, folder_id): """Expected folder was selected in frontend. @@ -521,6 +540,8 @@ class FrontendLoaderController(_BaseLoaderController): folder_id (str): Folder id. """ + pass + # Model wrapper calls @abstractmethod def get_project_items(self, sender=None): @@ -542,6 +563,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ProjectItem]: List of project items. """ + pass + @abstractmethod def get_folder_type_items(self, project_name, sender=None): """Folder type items for a project. @@ -560,6 +583,7 @@ class FrontendLoaderController(_BaseLoaderController): list[FolderTypeItem]: Folder type information. """ + pass @abstractmethod def get_task_items(self, project_name, folder_ids, sender=None): @@ -574,6 +598,7 @@ class FrontendLoaderController(_BaseLoaderController): list[TaskItem]: List of task items. """ + pass @abstractmethod def get_task_type_items(self, project_name, sender=None): @@ -593,6 +618,7 @@ class FrontendLoaderController(_BaseLoaderController): list[TaskTypeItem]: Task type information. """ + pass @abstractmethod def get_folder_labels(self, project_name, folder_ids): @@ -606,6 +632,7 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Optional[str]]: Folder labels by folder id. """ + pass @abstractmethod def get_project_status_items(self, project_name, sender=None): @@ -626,6 +653,8 @@ class FrontendLoaderController(_BaseLoaderController): list[StatusItem]: List of status items. """ + pass + @abstractmethod def get_product_items(self, project_name, folder_ids, sender=None): """Product items for folder ids. @@ -647,6 +676,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ProductItem]: List of product items. """ + pass + @abstractmethod def get_product_item(self, project_name, product_id): """Receive single product item. @@ -659,6 +690,8 @@ class FrontendLoaderController(_BaseLoaderController): Union[ProductItem, None]: Product info or None if not found. """ + pass + @abstractmethod def get_product_type_items(self, project_name): """Product type items for a project. @@ -672,6 +705,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ProductTypeItem]: List of product type items for a project. """ + pass + @abstractmethod def get_representation_items( self, project_name, version_ids, sender=None @@ -695,6 +730,8 @@ class FrontendLoaderController(_BaseLoaderController): list[RepreItem]: List of representation items. """ + pass + @abstractmethod def get_version_thumbnail_ids(self, project_name, version_ids): """Get thumbnail ids for version ids. @@ -707,6 +744,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, Any]]: Thumbnail id by version id. """ + pass + @abstractmethod def get_folder_thumbnail_ids(self, project_name, folder_ids): """Get thumbnail ids for folder ids. @@ -719,11 +758,14 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, Any]]: Thumbnail id by folder id. """ + pass + @abstractmethod def get_versions_representation_count( self, project_name, version_ids, sender=None ): - """Args: + """ + Args: project_name (str): Project name. version_ids (Iterable[str]): Version ids. sender (Optional[str]): Sender who requested the items. @@ -732,6 +774,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, int]: Representation count by version id. """ + pass + @abstractmethod def get_thumbnail_paths( self, @@ -754,6 +798,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, Union[str, None]]: Thumbnail path by entity id. """ + pass + # Selection model wrapper calls @abstractmethod def get_selected_project_name(self): @@ -765,6 +811,8 @@ class FrontendLoaderController(_BaseLoaderController): Union[str, None]: Selected project name. """ + pass + @abstractmethod def get_selected_folder_ids(self): """Get selected folder ids. @@ -775,6 +823,7 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected folder ids. """ + pass @abstractmethod def get_selected_task_ids(self): @@ -786,6 +835,7 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected folder ids. """ + pass @abstractmethod def set_selected_tasks(self, task_ids): @@ -795,6 +845,7 @@ class FrontendLoaderController(_BaseLoaderController): task_ids (Iterable[str]): Selected task ids. """ + pass @abstractmethod def get_selected_version_ids(self): @@ -806,6 +857,7 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected version ids. """ + pass @abstractmethod def get_selected_representation_ids(self): @@ -817,6 +869,8 @@ class FrontendLoaderController(_BaseLoaderController): list[str]: Selected representation ids. """ + pass + @abstractmethod def set_selected_project(self, project_name): """Set selected project. @@ -831,6 +885,8 @@ class FrontendLoaderController(_BaseLoaderController): project_name (Union[str, None]): Selected project name. """ + pass + @abstractmethod def set_selected_folders(self, folder_ids): """Set selected folders. @@ -846,6 +902,8 @@ class FrontendLoaderController(_BaseLoaderController): folder_ids (Iterable[str]): Selected folder ids. """ + pass + @abstractmethod def set_selected_versions(self, version_ids): """Set selected versions. @@ -862,6 +920,8 @@ class FrontendLoaderController(_BaseLoaderController): version_ids (Iterable[str]): Selected version ids. """ + pass + @abstractmethod def set_selected_representations(self, repre_ids): """Set selected representations. @@ -879,6 +939,8 @@ class FrontendLoaderController(_BaseLoaderController): repre_ids (Iterable[str]): Selected representation ids. """ + pass + # Load action items @abstractmethod def get_versions_action_items(self, project_name, version_ids): @@ -892,6 +954,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ActionItem]: List of action items. """ + pass + @abstractmethod def get_representations_action_items( self, project_name, representation_ids @@ -906,6 +970,8 @@ class FrontendLoaderController(_BaseLoaderController): list[ActionItem]: List of action items. """ + pass + @abstractmethod def trigger_action_item( self, @@ -938,6 +1004,8 @@ class FrontendLoaderController(_BaseLoaderController): representation_ids (Iterable[str]): Representation ids. """ + pass + @abstractmethod def change_products_group(self, project_name, product_ids, group_name): """Change group of products. @@ -956,6 +1024,8 @@ class FrontendLoaderController(_BaseLoaderController): group_name (str): New group name. """ + pass + @abstractmethod def fill_root_in_source(self, source): """Fill root in source path. @@ -965,6 +1035,8 @@ class FrontendLoaderController(_BaseLoaderController): rootless workfile path. """ + pass + # NOTE: Methods 'is_loaded_products_supported' and # 'is_standard_projects_filter_enabled' are both based on being in host # or not. Maybe we could implement only single method 'is_in_host'? @@ -976,6 +1048,8 @@ class FrontendLoaderController(_BaseLoaderController): bool: True if it is supported. """ + pass + @abstractmethod def is_standard_projects_filter_enabled(self): """Is standard projects filter enabled. @@ -988,6 +1062,8 @@ class FrontendLoaderController(_BaseLoaderController): current context project. """ + pass + # Site sync functions @abstractmethod def is_sitesync_enabled(self, project_name=None): @@ -1005,6 +1081,8 @@ class FrontendLoaderController(_BaseLoaderController): bool: True if site sync is enabled. """ + pass + @abstractmethod def get_active_site_icon_def(self, project_name): """Active site icon definition. @@ -1017,6 +1095,8 @@ class FrontendLoaderController(_BaseLoaderController): is not enabled for the project. """ + pass + @abstractmethod def get_remote_site_icon_def(self, project_name): """Remote site icon definition. @@ -1029,6 +1109,8 @@ class FrontendLoaderController(_BaseLoaderController): is not enabled for the project. """ + pass + @abstractmethod def get_version_sync_availability(self, project_name, version_ids): """Version sync availability. @@ -1041,6 +1123,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, tuple[int, int]]: Sync availability by version id. """ + pass + @abstractmethod def get_representations_sync_status( self, project_name, representation_ids @@ -1055,6 +1139,8 @@ class FrontendLoaderController(_BaseLoaderController): dict[str, tuple[int, int]]: Sync status by representation id. """ + pass + @abstractmethod def get_product_types_filter(self): """Return product type filter for current context. @@ -1062,3 +1148,5 @@ class FrontendLoaderController(_BaseLoaderController): Returns: ProductTypesFilter: Product type filter for current context """ + + pass From e8dcec0510dd78099f8ab2a3bea000cdd91a6ce5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 19 Jun 2025 10:52:34 +0200 Subject: [PATCH 096/108] Changed typing to support 3.7 We still support older Maya --- client/ayon_core/pipeline/load/plugins.py | 26 +++++++++++------------ 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 6a6c6c639f..e9ba5a37c3 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -1,7 +1,7 @@ from __future__ import annotations import os import logging -from typing import Any, Type +from typing import Any, Type, Optional from abc import abstractmethod from ayon_core.settings import get_project_settings @@ -275,10 +275,10 @@ class LoaderHookPlugin: def pre_load( self, context: dict, - name: str | None = None, - namespace: str | None = None, - options: dict | None = None, - plugin: LoaderPlugin | None = None, + name: Optional[str] = None, + namespace: Optional[str] = None, + options: Optional[dict] = None, + plugin: Optional[LoaderPlugin] = None, ): pass @@ -286,10 +286,10 @@ class LoaderHookPlugin: def post_load( self, context: dict, - name: str | None = None, - namespace: str | None = None, - options: dict | None = None, - plugin: LoaderPlugin | None = None, + name: Optional[str] = None, + namespace: Optional[str] = None, + options: Optional[dict] = None, + plugin: Optional[LoaderPlugin] = None, result: Any = None, ): pass @@ -299,7 +299,7 @@ class LoaderHookPlugin: self, container: dict, # (ayon:container-3.0) context: dict, - plugin: LoaderPlugin | None = None, + plugin: Optional[LoaderPlugin] = None, ): pass @@ -308,7 +308,7 @@ class LoaderHookPlugin: self, container: dict, # (ayon:container-3.0) context: dict, - plugin: LoaderPlugin | None = None, + plugin: Optional[LoaderPlugin] = None, result: Any = None, ): pass @@ -317,7 +317,7 @@ class LoaderHookPlugin: def pre_remove( self, container: dict, # (ayon:container-3.0) - plugin: LoaderPlugin | None = None, + plugin: Optional[LoaderPlugin] = None, ): pass @@ -325,7 +325,7 @@ class LoaderHookPlugin: def post_remove( self, container: dict, # (ayon:container-3.0) - plugin: LoaderPlugin | None = None, + plugin: Optional[LoaderPlugin] = None, result: Any = None, ): pass From b0e472ebe9be6741676860bdf9dd6852bc219a03 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 19 Jun 2025 11:18:34 +0200 Subject: [PATCH 097/108] Changed order of plugin, result Plugin is required so it makes sense to bump it up to first position and pass it before args --- client/ayon_core/pipeline/load/plugins.py | 35 +++++++++-------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index e9ba5a37c3..5f4056d2d5 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -274,59 +274,59 @@ class LoaderHookPlugin: @abstractmethod def pre_load( self, + plugin: LoaderPlugin, context: dict, name: Optional[str] = None, namespace: Optional[str] = None, options: Optional[dict] = None, - plugin: Optional[LoaderPlugin] = None, ): pass @abstractmethod def post_load( self, + plugin: LoaderPlugin, + result: Any, context: dict, name: Optional[str] = None, namespace: Optional[str] = None, options: Optional[dict] = None, - plugin: Optional[LoaderPlugin] = None, - result: Any = None, ): pass @abstractmethod def pre_update( self, + plugin: LoaderPlugin, container: dict, # (ayon:container-3.0) context: dict, - plugin: Optional[LoaderPlugin] = None, ): pass @abstractmethod def post_update( self, + plugin: LoaderPlugin, + result: Any, container: dict, # (ayon:container-3.0) context: dict, - plugin: Optional[LoaderPlugin] = None, - result: Any = None, ): pass @abstractmethod def pre_remove( self, + plugin: LoaderPlugin, container: dict, # (ayon:container-3.0) - plugin: Optional[LoaderPlugin] = None, ): pass @abstractmethod def post_remove( self, + plugin: LoaderPlugin, + result: Any, container: dict, # (ayon:container-3.0) - plugin: Optional[LoaderPlugin] = None, - result: Any = None, ): pass @@ -377,30 +377,21 @@ def add_hooks_to_loader( # Call pre_ on all hooks pre_hook_name = f"pre_{method_name}" - # Pass the LoaderPlugin instance to the hooks - hook_kwargs = kwargs.copy() - hook_kwargs["plugin"] = self - hooks: list[LoaderHookPlugin] = [] - for Hook in loader_class._load_hooks: - hook = Hook() # Instantiate the hook + for cls in loader_class._load_hooks: + hook = cls() # Instantiate the hook hooks.append(hook) pre_hook = getattr(hook, pre_hook_name, None) if callable(pre_hook): - pre_hook(*args, **hook_kwargs) - + pre_hook(hook, *args, **kwargs) # Call original method result = original_method(self, *args, **kwargs) - - # Add result to kwargs for post hooks from the original method - hook_kwargs["result"] = result - # Call post_ on all hooks post_hook_name = f"post_{method_name}" for hook in hooks: post_hook = getattr(hook, post_hook_name, None) if callable(post_hook): - post_hook(*args, **hook_kwargs) + post_hook(hook, result, *args, **kwargs) return result From d12273a6b82fc948fd1a1b2987c7dc7415826bec Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 19 Jun 2025 11:24:13 +0200 Subject: [PATCH 098/108] Removed None assignment --- client/ayon_core/pipeline/load/plugins.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 5f4056d2d5..73cc93797d 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -276,9 +276,9 @@ class LoaderHookPlugin: self, plugin: LoaderPlugin, context: dict, - name: Optional[str] = None, - namespace: Optional[str] = None, - options: Optional[dict] = None, + name: Optional[str], + namespace: Optional[str], + options: Optional[dict], ): pass @@ -288,9 +288,9 @@ class LoaderHookPlugin: plugin: LoaderPlugin, result: Any, context: dict, - name: Optional[str] = None, - namespace: Optional[str] = None, - options: Optional[dict] = None, + name: Optional[str], + namespace: Optional[str], + options: Optional[dict], ): pass From 68751b8f2287734dcc50e081a3ff6b6172ace11d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 19 Jun 2025 12:08:27 +0200 Subject: [PATCH 099/108] Fix wrong usage of Hook, should be LoaderPlugin Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 73cc93797d..d2e6cd1abf 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -391,7 +391,7 @@ def add_hooks_to_loader( for hook in hooks: post_hook = getattr(hook, post_hook_name, None) if callable(post_hook): - post_hook(hook, result, *args, **kwargs) + post_hook(self, result, *args, **kwargs) return result From e82716978d32638c9aa20aff5b5114a293469c7d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 19 Jun 2025 12:08:37 +0200 Subject: [PATCH 100/108] Fix wrong usage of Hook, should be LoaderPlugin Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index d2e6cd1abf..1dac8a4048 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -383,7 +383,7 @@ def add_hooks_to_loader( hooks.append(hook) pre_hook = getattr(hook, pre_hook_name, None) if callable(pre_hook): - pre_hook(hook, *args, **kwargs) + pre_hook(self, *args, **kwargs) # Call original method result = original_method(self, *args, **kwargs) # Call post_ on all hooks From 9738c2cc448371b5d18941c94268fdfa98b4558b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:54:19 +0200 Subject: [PATCH 101/108] Update client/ayon_core/pipeline/load/plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 966b418db8..ccee23c5c2 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -20,7 +20,7 @@ from .utils import get_representation_path_from_context class LoaderPlugin(list): """Load representation into host application""" - product_types: Optional[set[str]] = None + product_types: set[str] = set() product_base_types: Optional[set[str]] = None representations = set() extensions = {"*"} From 929418c8cbb82e3f67534884446844a70ac610ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:54:36 +0200 Subject: [PATCH 102/108] Update client/ayon_core/pipeline/load/plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/load/plugins.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index ccee23c5c2..0d0b073ad7 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -124,12 +124,6 @@ class LoaderPlugin(list): Returns: bool: Is loader compatible for context. """ - """ - product_types: set[str] = set() - product_base_types: set[str] = set() - representations = set() - extensions = {"*"} - """ plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types From deacb2853e429296b1603d949b8b5006c1821ef6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 20 Jun 2025 16:13:07 +0200 Subject: [PATCH 103/108] :recycle: refactor filtering and add some tests --- client/ayon_core/pipeline/load/plugins.py | 20 +++-- .../ayon_core/pipeline/load/test_loaders.py | 88 +++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/load/test_loaders.py diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 39343b76c6..5725133432 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -129,6 +129,10 @@ class LoaderPlugin(list): plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types plugin_product_base_types = cls.product_base_types + # If product type isn't defined on the loader plugin, + # then we will use the product types. + plugin_product_filter = ( + plugin_product_base_types or plugin_product_types) repre_entity = context.get("representation") product_entity = context["product"] @@ -136,8 +140,8 @@ class LoaderPlugin(list): # then loader is not compatible with any context. if ( not plugin_repre_names - or (not plugin_product_types and not plugin_product_base_types) - or not cls.extensions + and not plugin_product_filter + and not cls.extensions ): return False @@ -148,7 +152,7 @@ class LoaderPlugin(list): # Check the compatibility with the representation names. plugin_repre_names = set(plugin_repre_names) - if not plugin_repre_names or ( + if ( "*" not in plugin_repre_names and repre_entity["name"] not in plugin_repre_names ): @@ -169,8 +173,6 @@ class LoaderPlugin(list): if product_filter is None: product_filter = product_type - # If no product type isn't defined on the loader plugin, - # then we will use the product types. plugin_product_filter = ( plugin_product_base_types or plugin_product_types) @@ -179,6 +181,14 @@ class LoaderPlugin(list): if "*" in plugin_product_filter: return True + # compatibility with legacy loader + if cls.product_base_types is None and product_base_type: + cls.log.error( + f"Loader {cls.__name__} is doesn't specify " + "`product_base_types` but product entity has " + f"`productBaseType` defined as `{product_base_type}`. " + ) + return product_filter in plugin_product_filter @classmethod diff --git a/tests/client/ayon_core/pipeline/load/test_loaders.py b/tests/client/ayon_core/pipeline/load/test_loaders.py new file mode 100644 index 0000000000..490efe1b1e --- /dev/null +++ b/tests/client/ayon_core/pipeline/load/test_loaders.py @@ -0,0 +1,88 @@ +"""Test loaders in the pipeline module.""" + +from ayon_core.pipeline.load import LoaderPlugin + + +def test_is_compatible_loader(): + """Test if a loader is compatible with a given representation.""" + from ayon_core.pipeline.load import is_compatible_loader + + # Create a mock representation context + context = { + "loader": "test_loader", + "representation": {"name": "test_representation"}, + } + + # Create a mock loader plugin + class MockLoader(LoaderPlugin): + name = "test_loader" + version = "1.0.0" + + def is_compatible_loader(self, context): + return True + + # Check compatibility + assert is_compatible_loader(MockLoader(), context) is True + + +def test_complex_is_compatible_loader(): + """Test if a loader is compatible with a complex representation.""" + from ayon_core.pipeline.load import is_compatible_loader + + # Create a mock complex representation context + context = { + "loader": "complex_loader", + "representation": { + "name": "complex_representation", + "extension": "exr" + }, + "additional_data": {"key": "value"}, + "product": { + "name": "complex_product", + "productType": "foo", + "productBaseType": "bar", + }, + } + + # Create a mock loader plugin + class ComplexLoaderA(LoaderPlugin): + name = "complex_loaderA" + + # False because the loader doesn't specify any compatibility (missing + # wildcard for product type and product base type) + assert is_compatible_loader(ComplexLoaderA(), context) is False + + class ComplexLoaderB(LoaderPlugin): + name = "complex_loaderB" + product_types = {"*"} + representations = {"*"} + + # True, it is compatible with any product type + assert is_compatible_loader(ComplexLoaderB(), context) is True + + class ComplexLoaderC(LoaderPlugin): + name = "complex_loaderC" + product_base_types = {"*"} + representations = {"*"} + + # True, it is compatible with any product base type + assert is_compatible_loader(ComplexLoaderC(), context) is True + + class ComplexLoaderD(LoaderPlugin): + name = "complex_loaderD" + product_types = {"foo"} + representations = {"*"} + + # legacy loader defining compatibility only with product type + # is compatible provided the same product type is defined in context + assert is_compatible_loader(ComplexLoaderD(), context) is False + + class ComplexLoaderE(LoaderPlugin): + name = "complex_loaderE" + product_types = {"foo"} + representations = {"*"} + + # remove productBaseType from context to simulate legacy behavior + context["product"].pop("productBaseType", None) + + assert is_compatible_loader(ComplexLoaderE(), context) is True From 8b98c56ee87959a36a82de575d888de1f89447d1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 23 Jun 2025 09:14:42 +0200 Subject: [PATCH 104/108] Handles missing data keys safely Uses `get` method to safely access optional entities in the data dictionary, preventing potential KeyError when keys are absent. --- client/ayon_core/hooks/pre_global_host_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hooks/pre_global_host_data.py b/client/ayon_core/hooks/pre_global_host_data.py index 23f725901c..83c4118136 100644 --- a/client/ayon_core/hooks/pre_global_host_data.py +++ b/client/ayon_core/hooks/pre_global_host_data.py @@ -32,8 +32,8 @@ class GlobalHostDataHook(PreLaunchHook): "app": app, "project_entity": self.data["project_entity"], - "folder_entity": self.data["folder_entity"], - "task_entity": self.data["task_entity"], + "folder_entity": self.data.get("folder_entity"), + "task_entity": self.data.get("task_entity"), "anatomy": self.data["anatomy"], From 6de993af25a416a4f1e6bb515da9a884189eae67 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:06:53 +0200 Subject: [PATCH 105/108] remove usage of deprecated 'HighQualityAntialiasing' flag --- .../ayon_core/tools/publisher/widgets/screenshot_widget.py | 2 -- .../ayon_core/tools/publisher/widgets/thumbnail_widget.py | 6 ------ client/ayon_core/tools/utils/color_widgets/color_inputs.py | 2 +- .../ayon_core/tools/utils/color_widgets/color_triangle.py | 2 +- client/ayon_core/tools/utils/lib.py | 3 --- client/ayon_core/tools/utils/sliders.py | 2 +- client/ayon_core/tools/utils/thumbnail_paint_widget.py | 6 ------ client/ayon_core/tools/utils/widgets.py | 4 ---- 8 files changed, 3 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py index 0706299f32..e9749c5b07 100644 --- a/client/ayon_core/tools/publisher/widgets/screenshot_widget.py +++ b/client/ayon_core/tools/publisher/widgets/screenshot_widget.py @@ -348,8 +348,6 @@ class ScreenMarquee(QtCore.QObject): # QtGui.QPainter.Antialiasing # | QtGui.QPainter.SmoothPixmapTransform # ) - # if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - # render_hints |= QtGui.QPainter.HighQualityAntialiasing # pix_painter.setRenderHints(render_hints) # for item in screen_pixes: # (screen_pix, offset) = item diff --git a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py index 261dcfb43d..f767fdf325 100644 --- a/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py +++ b/client/ayon_core/tools/publisher/widgets/thumbnail_widget.py @@ -135,8 +135,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing pix_painter.setRenderHints(render_hints) pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) @@ -171,8 +169,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing pix_painter.setRenderHints(render_hints) tiled_rect = QtCore.QRectF( @@ -265,8 +261,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing final_painter.setRenderHints(render_hints) diff --git a/client/ayon_core/tools/utils/color_widgets/color_inputs.py b/client/ayon_core/tools/utils/color_widgets/color_inputs.py index 795b80fc1e..5a1c2dc50b 100644 --- a/client/ayon_core/tools/utils/color_widgets/color_inputs.py +++ b/client/ayon_core/tools/utils/color_widgets/color_inputs.py @@ -65,7 +65,7 @@ class AlphaSlider(QtWidgets.QSlider): painter.fillRect(event.rect(), QtCore.Qt.transparent) - painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + painter.setRenderHint(QtGui.QPainter.Antialiasing) horizontal = self.orientation() == QtCore.Qt.Horizontal diff --git a/client/ayon_core/tools/utils/color_widgets/color_triangle.py b/client/ayon_core/tools/utils/color_widgets/color_triangle.py index 290a33f0b0..7691c3e78d 100644 --- a/client/ayon_core/tools/utils/color_widgets/color_triangle.py +++ b/client/ayon_core/tools/utils/color_widgets/color_triangle.py @@ -261,7 +261,7 @@ class QtColorTriangle(QtWidgets.QWidget): pix = self.bg_image.copy() pix_painter = QtGui.QPainter(pix) - pix_painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + pix_painter.setRenderHint(QtGui.QPainter.Antialiasing) trigon_path = QtGui.QPainterPath() trigon_path.moveTo(self.point_a) diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index f7919a3317..a99c46199b 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -118,9 +118,6 @@ def paint_image_with_color(image, color): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - # Deprecated since 5.14 - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing painter.setRenderHints(render_hints) painter.setClipRegion(alpha_region) diff --git a/client/ayon_core/tools/utils/sliders.py b/client/ayon_core/tools/utils/sliders.py index ea1e01b9ea..c762b6ade0 100644 --- a/client/ayon_core/tools/utils/sliders.py +++ b/client/ayon_core/tools/utils/sliders.py @@ -58,7 +58,7 @@ class NiceSlider(QtWidgets.QSlider): painter.fillRect(event.rect(), QtCore.Qt.transparent) - painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + painter.setRenderHint(QtGui.QPainter.Antialiasing) horizontal = self.orientation() == QtCore.Qt.Horizontal diff --git a/client/ayon_core/tools/utils/thumbnail_paint_widget.py b/client/ayon_core/tools/utils/thumbnail_paint_widget.py index 9dbc2bcdd0..e67b820417 100644 --- a/client/ayon_core/tools/utils/thumbnail_paint_widget.py +++ b/client/ayon_core/tools/utils/thumbnail_paint_widget.py @@ -205,8 +205,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing pix_painter.setRenderHints(render_hints) pix_painter.drawPixmap(pos_x, pos_y, scaled_pix) @@ -241,8 +239,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing pix_painter.setRenderHints(render_hints) tiled_rect = QtCore.QRectF( @@ -335,8 +331,6 @@ class ThumbnailPainterWidget(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing final_painter.setRenderHints(render_hints) diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index af0745af1f..388eb34cd1 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -624,8 +624,6 @@ class ClassicExpandBtnLabel(ExpandBtnLabel): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing painter.setRenderHints(render_hints) painter.drawPixmap(QtCore.QPoint(pos_x, pos_y), pixmap) painter.end() @@ -788,8 +786,6 @@ class PixmapButtonPainter(QtWidgets.QWidget): QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) - if hasattr(QtGui.QPainter, "HighQualityAntialiasing"): - render_hints |= QtGui.QPainter.HighQualityAntialiasing painter.setRenderHints(render_hints) if self._cached_pixmap is None: From c1b9eff2df4a2502c5f831c348ea0763fcafddf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 23 Jun 2025 16:59:45 +0200 Subject: [PATCH 106/108] :bug: fix comment and condition --- client/ayon_core/pipeline/load/plugins.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 5725133432..62fe8150ae 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -129,10 +129,12 @@ class LoaderPlugin(list): plugin_repre_names = cls.get_representations() plugin_product_types = cls.product_types plugin_product_base_types = cls.product_base_types - # If product type isn't defined on the loader plugin, + + # If the product base type isn't defined on the loader plugin, # then we will use the product types. - plugin_product_filter = ( - plugin_product_base_types or plugin_product_types) + plugin_product_filter = plugin_product_base_types + if plugin_product_filter is None: + plugin_product_filter = plugin_product_types repre_entity = context.get("representation") product_entity = context["product"] From 2f9cd88111196ba61632dfc4e6cdf918a32775f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:44:27 +0200 Subject: [PATCH 107/108] revert conditions --- client/ayon_core/pipeline/load/plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 62fe8150ae..05584e60e9 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -142,8 +142,8 @@ class LoaderPlugin(list): # then loader is not compatible with any context. if ( not plugin_repre_names - and not plugin_product_filter - and not cls.extensions + or not plugin_product_filter + or not cls.extensions ): return False From 4aefacaf44e567e2695dbe3c1db2b91f43b1ea35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:44:54 +0200 Subject: [PATCH 108/108] remove unncessary product base type filters redefinitins --- client/ayon_core/pipeline/load/plugins.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 05584e60e9..dc5bb0f66f 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -127,14 +127,16 @@ class LoaderPlugin(list): """ plugin_repre_names = cls.get_representations() - plugin_product_types = cls.product_types - plugin_product_base_types = cls.product_base_types # If the product base type isn't defined on the loader plugin, # then we will use the product types. - plugin_product_filter = plugin_product_base_types + plugin_product_filter = cls.product_base_types if plugin_product_filter is None: - plugin_product_filter = plugin_product_types + plugin_product_filter = cls.product_types + + if plugin_product_filter: + plugin_product_filter = set(plugin_product_filter) + repre_entity = context.get("representation") product_entity = context["product"] @@ -164,7 +166,6 @@ class LoaderPlugin(list): if not cls.has_valid_extension(repre_entity): return False - plugin_product_types = set(plugin_product_types) product_type = product_entity.get("productType") product_base_type = product_entity.get("productBaseType") @@ -175,9 +176,6 @@ class LoaderPlugin(list): if product_filter is None: product_filter = product_type - plugin_product_filter = ( - plugin_product_base_types or plugin_product_types) - # If wildcard is used in product types or base types, # then we will consider the loader compatible with any product type. if "*" in plugin_product_filter: