diff --git a/client/ayon_core/addon/README.md b/client/ayon_core/addon/README.md index a15e8bdc69..88c27db154 100644 --- a/client/ayon_core/addon/README.md +++ b/client/ayon_core/addon/README.md @@ -27,7 +27,7 @@ AYON addons should contain separated logic of specific kind of implementation, s - default interfaces are defined in `interfaces.py` ## IPluginPaths -- addon wants to add directory path/s to avalon or publish plugins +- addon wants to add directory path/s to publish, load, create or inventory plugins - addon must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"` - each key may contain list or string with a path to directory with plugins @@ -89,4 +89,4 @@ AYON addons should contain separated logic of specific kind of implementation, s ### TrayAddonsManager - inherits from `AddonsManager` -- has specific implementation for Pype Tray tool and handle `ITrayAddon` methods +- has specific implementation for AYON Tray and handle `ITrayAddon` methods diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index bbd5a486fe..42b53c59e3 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -741,7 +741,7 @@ class AddonsManager: addon_classes = [] for module in openpype_modules: - # Go through globals in `pype.modules` + # Go through globals in `ayon_core.modules` for name in dir(module): modules_item = getattr(module, name, None) # Filter globals that are not classes which inherit from diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index 4335a3f2d9..bc0a22382c 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -67,8 +67,6 @@ class Commands: install_ayon_plugins, get_global_context, ) - from ayon_core.tools.utils.host_tools import show_publish - from ayon_core.tools.utils.lib import qt_app_context # Register target and host import pyblish.api @@ -134,6 +132,8 @@ class Commands: print(plugin) if gui: + from ayon_core.tools.utils.host_tools import show_publish + from ayon_core.tools.utils.lib import qt_app_context with qt_app_context(): show_publish() else: diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_file_saved.py b/client/ayon_core/hosts/blender/plugins/publish/validate_file_saved.py index 6a053eb47b..aa73525555 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_file_saved.py @@ -37,7 +37,8 @@ class ValidateFileSaved(pyblish.api.ContextPlugin, if not context.data["currentFile"]: # File has not been saved at all and has no filename raise PublishValidationError( - "Current file is empty. Save the file before continuing." + "Current workfile has not been saved yet.\n" + "Save the workfile before continuing." ) # Do not validate workfile has unsaved changes if only instances diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py b/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py new file mode 100644 index 0000000000..752bc5fa58 --- /dev/null +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_model_uv_map1.py @@ -0,0 +1,94 @@ +import inspect +from typing import List + +import bpy + +import pyblish.api + +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError, + RepairAction +) +import ayon_core.hosts.blender.api.action + + +class ValidateModelMeshUvMap1( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin, +): + """Validate model mesh uvs are named `map1`. + + This is solely to get them to work nicely for the Maya pipeline. + """ + + order = ValidateContentsOrder + hosts = ["blender"] + families = ["model"] + label = "Mesh UVs named map1" + actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction, + RepairAction] + optional = True + enabled = False + + @classmethod + def get_invalid(cls, instance) -> List: + + invalid = [] + for obj in instance: + if obj.mode != "OBJECT": + cls.log.warning( + f"Mesh object {obj.name} should be in 'OBJECT' mode" + " to be properly checked." + ) + + obj_data = obj.data + if isinstance(obj_data, bpy.types.Mesh): + mesh = obj_data + + # Ignore mesh without UVs + if not mesh.uv_layers: + continue + + # If mesh has map1 all is ok + if mesh.uv_layers.get("map1"): + continue + + cls.log.warning( + f"Mesh object {obj.name} should be in 'OBJECT' mode" + " to be properly checked." + ) + invalid.append(obj) + + return invalid + + @classmethod + def repair(cls, instance): + for obj in cls.get_invalid(instance): + mesh = obj.data + + # Rename the first UV set to map1 + mesh.uv_layers[0].name = "map1" + + def process(self, instance): + if not self.is_active(instance.data): + return + + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + f"Meshes found in instance without valid UV's: {invalid}", + description=self.get_description() + ) + + def get_description(self): + return inspect.cleandoc( + """## Meshes must have map1 uv set + + To accompany a better Maya-focused pipeline with Alembics it is + expected that a Mesh has a `map1` UV set. Blender defaults to + a UV set named `UVMap` and thus needs to be renamed. + + """ + ) diff --git a/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py index 267eff47e4..465ec15d7b 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/blender/plugins/publish/validate_transform_zero.py @@ -1,3 +1,4 @@ +import inspect from typing import List import mathutils @@ -5,29 +6,26 @@ import bpy import pyblish.api +from ayon_core.hosts.blender.api import plugin, lib import ayon_core.hosts.blender.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, OptionalPyblishPluginMixin, - PublishValidationError + PublishValidationError, + RepairAction ) class ValidateTransformZero(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): - """Transforms can't have any values - - To solve this issue, try freezing the transforms. So long - as the transforms, rotation and scale values are zero, - you're all good. - - """ + """Transforms can't have any values""" order = ValidateContentsOrder hosts = ["blender"] families = ["model"] label = "Transform Zero" - actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction] + actions = [ayon_core.hosts.blender.api.action.SelectInvalidAction, + RepairAction] _identity = mathutils.Matrix() @@ -51,5 +49,46 @@ class ValidateTransformZero(pyblish.api.InstancePlugin, names = ", ".join(obj.name for obj in invalid) raise PublishValidationError( "Objects found in instance which do not" - f" have transform set to zero: {names}" + f" have transform set to zero: {names}", + description=self.get_description() ) + + @classmethod + def repair(cls, instance): + + invalid = cls.get_invalid(instance) + if not invalid: + return + + context = plugin.create_blender_context( + active=invalid[0], selected=invalid + ) + with lib.maintained_selection(): + with bpy.context.temp_override(**context): + plugin.deselect_all() + for obj in invalid: + obj.select_set(True) + + # TODO: Preferably this does allow custom pivot point locations + # and if so, this should likely apply to the delta instead + # using `bpy.ops.object.transforms_to_deltas(mode="ALL")` + bpy.ops.object.transform_apply(location=True, + rotation=True, + scale=True) + + def get_description(self): + return inspect.cleandoc( + """## Transforms can't have any values. + + The location, rotation and scale on the transform must be at + the default values. This also goes for the delta transforms. + + To solve this issue, try freezing the transforms: + - `Object` > `Apply` > `All Transforms` + + Using the Repair action directly will do the same. + + So long as the transforms, rotation and scale values are zero, + you're all good. + """ + ) diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py b/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py index abe670b691..52bb183663 100644 --- a/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py +++ b/client/ayon_core/hosts/celaction/plugins/publish/collect_render_path.py @@ -18,7 +18,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin): def process(self, instance): anatomy = instance.context.data["anatomy"] anatomy_data = copy.deepcopy(instance.data["anatomyData"]) - padding = anatomy.templates.get("frame_padding", 4) + padding = anatomy.templates_obj.frame_padding product_type = "render" anatomy_data.update({ "frame": f"%0{padding}d", @@ -28,15 +28,14 @@ class CollectRenderPath(pyblish.api.InstancePlugin): }) anatomy_data["product"]["type"] = product_type - anatomy_filled = anatomy.format(anatomy_data) - # get anatomy rendering keys r_anatomy_key = self.anatomy_template_key_render_files m_anatomy_key = self.anatomy_template_key_metadata # get folder and path for rendering images from celaction - render_dir = anatomy_filled[r_anatomy_key]["folder"] - render_path = anatomy_filled[r_anatomy_key]["path"] + r_template_item = anatomy.get_template_item("publish", r_anatomy_key) + render_dir = r_template_item["directory"].format_strict(anatomy_data) + render_path = r_template_item["path"].format_strict(anatomy_data) self.log.debug("__ render_path: `{}`".format(render_path)) # create dir if it doesnt exists @@ -51,11 +50,14 @@ class CollectRenderPath(pyblish.api.InstancePlugin): instance.data["path"] = render_path # get anatomy for published renders folder path - if anatomy_filled.get(m_anatomy_key): - instance.data["publishRenderMetadataFolder"] = anatomy_filled[ - m_anatomy_key]["folder"] - self.log.info("Metadata render path: `{}`".format( - instance.data["publishRenderMetadataFolder"] - )) + m_template_item = anatomy.get_template_item( + "publish", m_anatomy_key, default=None + ) + if m_template_item is not None: + metadata_path = m_template_item["directory"].format_strict( + anatomy_data + ) + instance.data["publishRenderMetadataFolder"] = metadata_path + self.log.info("Metadata render path: `{}`".format(metadata_path)) self.log.info(f"Render output path set to: `{render_path}`") diff --git a/client/ayon_core/hosts/flame/api/__init__.py b/client/ayon_core/hosts/flame/api/__init__.py index c00ee958b6..e2c5ee154a 100644 --- a/client/ayon_core/hosts/flame/api/__init__.py +++ b/client/ayon_core/hosts/flame/api/__init__.py @@ -1,5 +1,5 @@ """ -OpenPype Autodesk Flame api +AYON Autodesk Flame api """ from .constants import ( COLOR_MAP, diff --git a/client/ayon_core/hosts/flame/api/constants.py b/client/ayon_core/hosts/flame/api/constants.py index 1833031e13..04191c539d 100644 --- a/client/ayon_core/hosts/flame/api/constants.py +++ b/client/ayon_core/hosts/flame/api/constants.py @@ -1,14 +1,14 @@ """ -OpenPype Flame api constances +AYON Flame api constances """ -# OpenPype marker workflow variables +# AYON marker workflow variables MARKER_NAME = "OpenPypeData" MARKER_DURATION = 0 MARKER_COLOR = "cyan" MARKER_PUBLISH_DEFAULT = False -# OpenPype color definitions +# AYON color definitions COLOR_MAP = { "red": (1.0, 0.0, 0.0), "orange": (1.0, 0.5, 0.0), diff --git a/client/ayon_core/hosts/flame/api/pipeline.py b/client/ayon_core/hosts/flame/api/pipeline.py index a902b9ee73..4578d7bb4b 100644 --- a/client/ayon_core/hosts/flame/api/pipeline.py +++ b/client/ayon_core/hosts/flame/api/pipeline.py @@ -38,12 +38,12 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - log.info("OpenPype Flame plug-ins registered ...") + log.info("AYON Flame plug-ins registered ...") # register callback for switching publishable pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) - log.info("OpenPype Flame host installed ...") + log.info("AYON Flame host installed ...") def uninstall(): @@ -57,7 +57,7 @@ def uninstall(): # register callback for switching publishable pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) - log.info("OpenPype Flame host uninstalled ...") + log.info("AYON Flame host uninstalled ...") def containerise(flame_clip_segment, diff --git a/client/ayon_core/hosts/flame/api/plugin.py b/client/ayon_core/hosts/flame/api/plugin.py index c5667eb75a..c57d021c69 100644 --- a/client/ayon_core/hosts/flame/api/plugin.py +++ b/client/ayon_core/hosts/flame/api/plugin.py @@ -38,7 +38,7 @@ class CreatorWidget(QtWidgets.QDialog): | QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowStaysOnTopHint ) - self.setWindowTitle(name or "Pype Creator Input") + self.setWindowTitle(name or "AYON Creator Input") self.resize(500, 700) # Where inputs and labels are set diff --git a/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py b/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py index cffc6ec782..42b9257cbe 100644 --- a/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py +++ b/client/ayon_core/hosts/flame/api/scripts/wiretap_com.py @@ -61,7 +61,7 @@ class WireTapCom(object): def get_launch_args( self, project_name, project_data, user_name, *args, **kwargs): - """Forming launch arguments for OpenPype launcher. + """Forming launch arguments for AYON launcher. Args: project_name (str): name of project diff --git a/client/ayon_core/hosts/flame/api/utils.py b/client/ayon_core/hosts/flame/api/utils.py index 91584456a6..b76dd92ada 100644 --- a/client/ayon_core/hosts/flame/api/utils.py +++ b/client/ayon_core/hosts/flame/api/utils.py @@ -11,7 +11,7 @@ log = Logger.get_logger(__name__) def _sync_utility_scripts(env=None): """ Synchronizing basic utlility scripts for flame. - To be able to run start OpenPype within Flame we have to copy + To be able to run start AYON within Flame we have to copy all utility_scripts and additional FLAME_SCRIPT_DIR into `/opt/Autodesk/shared/python`. This will be always synchronizing those folders. @@ -124,7 +124,7 @@ def setup(env=None): # synchronize resolve utility scripts _sync_utility_scripts(env) - log.info("Flame OpenPype wrapper has been installed") + log.info("Flame AYON wrapper has been installed") def get_flame_version(): diff --git a/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py b/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py index b7fc431352..1ff7ad7ccf 100644 --- a/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py +++ b/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py @@ -72,7 +72,7 @@ class FlamePrelaunch(PreLaunchHook): project_data = { "Name": project_entity["name"], "Nickname": project_entity["code"], - "Description": "Created by OpenPype", + "Description": "Created by AYON", "SetupDir": project_entity["name"], "FrameWidth": int(width), "FrameHeight": int(height), diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py index 5c5bb0b4a1..ce023a9e4d 100644 --- a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py +++ b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py @@ -79,7 +79,7 @@ class FlameBabyPublisherPanel(object): # creating ui self.window.setMinimumSize(1500, 600) - self.window.setWindowTitle('OpenPype: Baby-publisher') + self.window.setWindowTitle('AYON: Baby-publisher') self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.window.setFocusPolicy(QtCore.Qt.StrongFocus) diff --git a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py index 4675d163e3..76d74b5970 100644 --- a/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py +++ b/client/ayon_core/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py @@ -31,7 +31,7 @@ def scope_sequence(selection): def get_media_panel_custom_ui_actions(): return [ { - "name": "OpenPype: Baby-publisher", + "name": "AYON: Baby-publisher", "actions": [ { "name": "Create Shots", diff --git a/client/ayon_core/hosts/flame/startup/openpype_in_flame.py b/client/ayon_core/hosts/flame/startup/openpype_in_flame.py index cf0a24ede2..b9cbf9700b 100644 --- a/client/ayon_core/hosts/flame/startup/openpype_in_flame.py +++ b/client/ayon_core/hosts/flame/startup/openpype_in_flame.py @@ -12,7 +12,7 @@ from ayon_core.pipeline import ( def openpype_install(): - """Registering OpenPype in context + """Registering AYON in context """ install_host(opfapi) print("Registered host: {}".format(registered_host())) @@ -28,7 +28,7 @@ def exeption_handler(exctype, value, _traceback): tb (str): traceback to show """ import traceback - msg = "OpenPype: Python exception {} in {}".format(value, exctype) + msg = "AYON: Python exception {} in {}".format(value, exctype) mbox = QtWidgets.QMessageBox() mbox.setText(msg) mbox.setDetailedText( diff --git a/client/ayon_core/hosts/fusion/api/__init__.py b/client/ayon_core/hosts/fusion/api/__init__.py index ddd718e606..d2feee6d23 100644 --- a/client/ayon_core/hosts/fusion/api/__init__.py +++ b/client/ayon_core/hosts/fusion/api/__init__.py @@ -15,7 +15,7 @@ from .lib import ( comp_lock_and_undo_chunk ) -from .menu import launch_openpype_menu +from .menu import launch_ayon_menu __all__ = [ @@ -35,5 +35,5 @@ __all__ = [ "comp_lock_and_undo_chunk", # menu - "launch_openpype_menu", + "launch_ayon_menu", ] diff --git a/client/ayon_core/hosts/fusion/api/menu.py b/client/ayon_core/hosts/fusion/api/menu.py index 642287eb10..6a64ad2120 100644 --- a/client/ayon_core/hosts/fusion/api/menu.py +++ b/client/ayon_core/hosts/fusion/api/menu.py @@ -28,9 +28,9 @@ self = sys.modules[__name__] self.menu = None -class OpenPypeMenu(QtWidgets.QWidget): +class AYONMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): - super(OpenPypeMenu, self).__init__(*args, **kwargs) + super(AYONMenu, self).__init__(*args, **kwargs) self.setObjectName(f"{MENU_LABEL}Menu") @@ -125,7 +125,7 @@ class OpenPypeMenu(QtWidgets.QWidget): self._pulse = FusionPulse(parent=self) self._pulse.start() - # Detect Fusion events as OpenPype events + # Detect Fusion events as AYON events self._event_handler = FusionEventHandler(parent=self) self._event_handler.start() @@ -174,16 +174,16 @@ class OpenPypeMenu(QtWidgets.QWidget): set_current_context_framerange() -def launch_openpype_menu(): +def launch_ayon_menu(): app = get_qt_app() - pype_menu = OpenPypeMenu() + ayon_menu = AYONMenu() stylesheet = load_stylesheet() - pype_menu.setStyleSheet(stylesheet) + ayon_menu.setStyleSheet(stylesheet) - pype_menu.show() - self.menu = pype_menu + ayon_menu.show() + self.menu = ayon_menu result = app.exec_() print("Shutting down..") diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index 3bb66619a9..50157cfae6 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -70,7 +70,7 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "fusion" def install(self): - """Install fusion-specific functionality of OpenPype. + """Install fusion-specific functionality of AYON. This is where you install menus and register families, data and loaders into fusion. @@ -177,7 +177,7 @@ def on_after_open(event): if any_outdated_containers(): log.warning("Scene has outdated content.") - # Find OpenPype menu to attach to + # Find AYON menu to attach to from . import menu def _on_show_scene_inventory(): @@ -326,9 +326,9 @@ class FusionEventThread(QtCore.QThread): class FusionEventHandler(QtCore.QObject): - """Emits OpenPype events based on Fusion events captured in a QThread. + """Emits AYON events based on Fusion events captured in a QThread. - This will emit the following OpenPype events based on Fusion actions: + This will emit the following AYON events based on Fusion actions: save: Comp_Save, Comp_SaveAs open: Comp_Opened new: Comp_New @@ -374,7 +374,7 @@ class FusionEventHandler(QtCore.QObject): self._event_thread.stop() def _on_event(self, event): - """Handle Fusion events to emit OpenPype events""" + """Handle Fusion events to emit AYON events""" if not event: return diff --git a/client/ayon_core/hosts/fusion/api/plugin.py b/client/ayon_core/hosts/fusion/api/plugin.py index f63b5eaec3..492841f967 100644 --- a/client/ayon_core/hosts/fusion/api/plugin.py +++ b/client/ayon_core/hosts/fusion/api/plugin.py @@ -133,7 +133,7 @@ class GenericCreateSaver(Creator): formatting_data = deepcopy(data) # get frame padding from anatomy templates - frame_padding = self.project_anatomy.templates["frame_padding"] + frame_padding = self.project_anatomy.templates_obj.frame_padding # get output format ext = data["creator_attributes"]["image_format"] diff --git a/client/ayon_core/hosts/fusion/deploy/MenuScripts/README.md b/client/ayon_core/hosts/fusion/deploy/MenuScripts/README.md index f87eaea4a2..e291b8d8f2 100644 --- a/client/ayon_core/hosts/fusion/deploy/MenuScripts/README.md +++ b/client/ayon_core/hosts/fusion/deploy/MenuScripts/README.md @@ -1,6 +1,6 @@ -### OpenPype deploy MenuScripts +### AYON deploy MenuScripts Note that this `MenuScripts` is not an official Fusion folder. -OpenPype only uses this folder in `{fusion}/deploy/` to trigger the OpenPype menu actions. +AYON only uses this folder in `{fusion}/deploy/` to trigger the AYON menu actions. They are used in the actions defined in `.fu` files in `{fusion}/deploy/Config`. \ No newline at end of file diff --git a/client/ayon_core/hosts/fusion/deploy/MenuScripts/launch_menu.py b/client/ayon_core/hosts/fusion/deploy/MenuScripts/launch_menu.py index 23b02b1b69..640f78eeb8 100644 --- a/client/ayon_core/hosts/fusion/deploy/MenuScripts/launch_menu.py +++ b/client/ayon_core/hosts/fusion/deploy/MenuScripts/launch_menu.py @@ -35,7 +35,7 @@ def main(env): log = Logger.get_logger(__name__) log.info(f"Registered host: {registered_host()}") - menu.launch_openpype_menu() + menu.launch_ayon_menu() # Initiate a QTimer to check if Fusion is still alive every X interval # If Fusion is not found - kill itself diff --git a/client/ayon_core/hosts/fusion/hooks/pre_fusion_profile_hook.py b/client/ayon_core/hosts/fusion/hooks/pre_fusion_profile_hook.py index 5aa2783129..10b1c9c45d 100644 --- a/client/ayon_core/hosts/fusion/hooks/pre_fusion_profile_hook.py +++ b/client/ayon_core/hosts/fusion/hooks/pre_fusion_profile_hook.py @@ -19,7 +19,7 @@ class FusionCopyPrefsPrelaunch(PreLaunchHook): Prepares local Fusion profile directory, copies existing Fusion profile. This also sets FUSION MasterPrefs variable, which is used to apply Master.prefs file to override some Fusion profile settings to: - - enable the OpenPype menu + - enable the AYON menu - force Python 3 over Python 2 - force English interface Master.prefs is defined in openpype/hosts/fusion/deploy/fusion_shared.prefs diff --git a/client/ayon_core/hosts/fusion/hooks/pre_fusion_setup.py b/client/ayon_core/hosts/fusion/hooks/pre_fusion_setup.py index 7eaf2ddc02..5e97ae3de1 100644 --- a/client/ayon_core/hosts/fusion/hooks/pre_fusion_setup.py +++ b/client/ayon_core/hosts/fusion/hooks/pre_fusion_setup.py @@ -13,7 +13,7 @@ from ayon_core.hosts.fusion import ( class FusionPrelaunch(PreLaunchHook): """ - Prepares OpenPype Fusion environment. + Prepares AYON Fusion environment. Requires correct Python home variable to be defined in the environment settings for Fusion to point at a valid Python 3 build for Fusion. Python3 versions that are supported by Fusion: diff --git a/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py b/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py index 8110898ae9..729843d078 100644 --- a/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py +++ b/client/ayon_core/hosts/fusion/plugins/create/create_image_saver.py @@ -1,7 +1,6 @@ from ayon_core.lib import NumberDef from ayon_core.hosts.fusion.api.plugin import GenericCreateSaver -from ayon_core.hosts.fusion.api import get_current_comp class CreateImageSaver(GenericCreateSaver): diff --git a/client/ayon_core/hosts/harmony/api/README.md b/client/ayon_core/hosts/harmony/api/README.md index 6d1e400476..7ac185638a 100644 --- a/client/ayon_core/hosts/harmony/api/README.md +++ b/client/ayon_core/hosts/harmony/api/README.md @@ -204,7 +204,7 @@ class CreateComposite(harmony.Creator): name = "compositeDefault" label = "Composite" - product_type = "mindbender.template" + product_type = "template" def __init__(self, *args, **kwargs): super(CreateComposite, self).__init__(*args, **kwargs) @@ -221,7 +221,7 @@ class CreateRender(harmony.Creator): name = "writeDefault" label = "Write" - product_type = "mindbender.imagesequence" + product_type = "render" node_type = "WRITE" def __init__(self, *args, **kwargs): @@ -304,7 +304,7 @@ class ExtractImage(pyblish.api.InstancePlugin): label = "Extract Image Sequence" order = pyblish.api.ExtractorOrder hosts = ["harmony"] - families = ["mindbender.imagesequence"] + families = ["render"] def process(self, instance): project_path = harmony.send( @@ -582,8 +582,16 @@ class ImageSequenceLoader(load.LoaderPlugin): """Load images Stores the imported asset in a container named after the asset. """ - product_types = {"mindbender.imagesequence"} + product_types = { + "shot", + "render", + "image", + "plate", + "reference", + "review", + } representations = ["*"] + extensions = {"jpeg", "png", "jpg"} def load(self, context, name=None, namespace=None, data=None): files = [] diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py b/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py index 7bf634f00c..c7132ce373 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py @@ -50,11 +50,11 @@ class ImportTemplateLoader(load.LoaderPlugin): self.__class__.__name__ ) - def update(self, container, context): - pass + def update(self, container, context): + pass - def remove(self, container): - pass + def remove(self, container): + pass class ImportWorkfileLoader(ImportTemplateLoader): diff --git a/client/ayon_core/hosts/hiero/api/lib.py b/client/ayon_core/hosts/hiero/api/lib.py index c46269b532..8e08e8cbf3 100644 --- a/client/ayon_core/hosts/hiero/api/lib.py +++ b/client/ayon_core/hosts/hiero/api/lib.py @@ -632,7 +632,9 @@ def sync_avalon_data_to_workfile(): project_name = get_current_project_name() anatomy = Anatomy(project_name) - work_template = anatomy.templates["work"]["path"] + work_template = anatomy.get_template_item( + "work", "default", "path" + ) work_root = anatomy.root_value_for_template(work_template) active_project_root = ( os.path.join(work_root, project_name) @@ -825,7 +827,7 @@ class PublishAction(QtWidgets.QAction): # root_node = hiero.core.nuke.RootNode() # # anatomy = Anatomy(get_current_project_name()) -# work_template = anatomy.templates["work"]["path"] +# work_template = anatomy.get_template_item("work", "default", "path") # root_path = anatomy.root_value_for_template(work_template) # # nuke_script.addNode(root_node) diff --git a/client/ayon_core/hosts/hiero/api/plugin.py b/client/ayon_core/hosts/hiero/api/plugin.py index 6a665dc9c5..4878368716 100644 --- a/client/ayon_core/hosts/hiero/api/plugin.py +++ b/client/ayon_core/hosts/hiero/api/plugin.py @@ -45,7 +45,7 @@ class CreatorWidget(QtWidgets.QDialog): | QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowStaysOnTopHint ) - self.setWindowTitle(name or "Pype Creator Input") + self.setWindowTitle(name or "AYON Creator Input") self.resize(500, 700) # Where inputs and labels are set diff --git a/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py index 90ea9ef50f..62e7041286 100644 --- a/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/hiero/plugins/create/create_shot_clip.py @@ -16,7 +16,7 @@ class CreateShotClip(phiero.Creator): gui_tracks = [track.name() for track in phiero.get_current_sequence().videoTracks()] - gui_name = "Pype publish attributes creator" + gui_name = "AYON publish attributes creator" gui_info = "Define sequential rename and fill hierarchy data." gui_inputs = { "renameHierarchy": { diff --git a/client/ayon_core/hosts/houdini/api/plugin.py b/client/ayon_core/hosts/houdini/api/plugin.py index 0809f4e566..a9c8c313b9 100644 --- a/client/ayon_core/hosts/houdini/api/plugin.py +++ b/client/ayon_core/hosts/houdini/api/plugin.py @@ -19,10 +19,6 @@ from ayon_core.lib import BoolDef from .lib import imprint, read, lsattr, add_self_publish_button -class OpenPypeCreatorError(CreatorError): - pass - - class Creator(LegacyCreator): """Creator plugin to create instances in Houdini @@ -92,8 +88,8 @@ class Creator(LegacyCreator): except hou.Error as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError("Creator error: {}".format(er)), + CreatorError, + CreatorError("Creator error: {}".format(er)), sys.exc_info()[2]) @@ -147,7 +143,6 @@ class HoudiniCreatorBase(object): def create_instance_node( folder_path, node_name, parent, node_type="geometry" ): - # type: (str, str, str) -> hou.Node """Create node representing instance. Arguments: @@ -210,8 +205,8 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): except hou.Error as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError("Creator error: {}".format(er)), + CreatorError, + CreatorError("Creator error: {}".format(er)), sys.exc_info()[2]) def lock_parameters(self, node, parameters): diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index c16c95a270..b307293dc8 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -2,6 +2,7 @@ """Creator plugin for creating publishable Houdini Digital Assets.""" import ayon_api +from ayon_core.pipeline import CreatorError from ayon_core.hosts.houdini.api import plugin import hou @@ -16,7 +17,7 @@ class CreateHDA(plugin.HoudiniCreator): maintain_selection = False def _check_existing(self, folder_path, product_name): - # type: (str) -> bool + # type: (str, str) -> bool """Check if existing product name versions already exists.""" # Get all products of the current folder project_name = self.project_name @@ -52,7 +53,7 @@ class CreateHDA(plugin.HoudiniCreator): # if node type has not its definition, it is not user # created hda. We test if hda can be created from the node. if not to_hda.canCreateDigitalAsset(): - raise plugin.OpenPypeCreatorError( + raise CreatorError( "cannot create hda from node {}".format(to_hda)) hda_node = to_hda.createDigitalAsset( @@ -61,7 +62,7 @@ class CreateHDA(plugin.HoudiniCreator): ) hda_node.layoutChildren() elif self._check_existing(folder_path, node_name): - raise plugin.OpenPypeCreatorError( + raise CreatorError( ("product {} is already published with different HDA" "definition.").format(node_name)) else: diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py index 3d6d657cf0..ba0795a26e 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_redshift_rop.py @@ -2,6 +2,7 @@ """Creator plugin to create Redshift ROP.""" import hou # noqa +from ayon_core.pipeline import CreatorError from ayon_core.hosts.houdini.api import plugin from ayon_core.lib import EnumDef, BoolDef @@ -42,7 +43,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): "Redshift_IPR", node_name=f"{basename}_IPR" ) except hou.OperationFailed as e: - raise plugin.OpenPypeCreatorError( + raise CreatorError( ( "Cannot create Redshift node. Is Redshift " "installed and enabled?" diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py b/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py index 739796dc7c..6b2396bffb 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_vray_rop.py @@ -3,7 +3,7 @@ import hou from ayon_core.hosts.houdini.api import plugin -from ayon_core.pipeline import CreatedInstance +from ayon_core.pipeline import CreatedInstance, CreatorError from ayon_core.lib import EnumDef, BoolDef @@ -42,7 +42,7 @@ class CreateVrayROP(plugin.HoudiniCreator): "vray", node_name=basename + "_IPR" ) except hou.OperationFailed: - raise plugin.OpenPypeCreatorError( + raise CreatorError( "Cannot create Vray render node. " "Make sure Vray installed and enabled!" ) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_image.py b/client/ayon_core/hosts/houdini/plugins/load/load_image.py index b77e4f662a..0429b1c3fe 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_image.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_image.py @@ -1,4 +1,5 @@ import os +import re from ayon_core.pipeline import ( load, @@ -44,7 +45,14 @@ def get_image_avalon_container(): class ImageLoader(load.LoaderPlugin): """Load images into COP2""" - product_types = {"imagesequence"} + product_types = { + "imagesequence", + "review", + "render", + "plate", + "image", + "online", + } label = "Load Image (COP2)" representations = ["*"] order = -10 @@ -55,10 +63,8 @@ class ImageLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): # Format file name, Houdini only wants forward slashes - file_path = self.filepath_from_context(context) - file_path = os.path.normpath(file_path) - file_path = file_path.replace("\\", "/") - file_path = self._get_file_sequence(file_path) + path = self.filepath_from_context(context) + path = self.format_path(path, representation=context["representation"]) # Get the root node parent = get_image_avalon_container() @@ -70,7 +76,10 @@ class ImageLoader(load.LoaderPlugin): node = parent.createNode("file", node_name=node_name) node.moveToGoodPosition() - node.setParms({"filename1": file_path}) + parms = {"filename1": path} + parms.update(self.get_colorspace_parms(context["representation"])) + + node.setParms(parms) # Imprint it manually data = { @@ -93,16 +102,17 @@ class ImageLoader(load.LoaderPlugin): # Update the file path file_path = get_representation_path(repre_entity) - file_path = file_path.replace("\\", "/") - file_path = self._get_file_sequence(file_path) + file_path = self.format_path(file_path, repre_entity) + + parms = { + "filename1": file_path, + "representation": repre_entity["id"], + } + + parms.update(self.get_colorspace_parms(repre_entity)) # Update attributes - node.setParms( - { - "filename1": file_path, - "representation": repre_entity["id"], - } - ) + node.setParms(parms) def remove(self, container): @@ -119,14 +129,58 @@ class ImageLoader(load.LoaderPlugin): if not parent.children(): parent.destroy() - def _get_file_sequence(self, file_path): - root = os.path.dirname(file_path) - files = sorted(os.listdir(root)) + @staticmethod + def format_path(path, representation): + """Format file path correctly for single image or sequence.""" + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) - first_fname = files[0] - prefix, padding, suffix = first_fname.rsplit(".", 2) - fname = ".".join([prefix, "$F{}".format(len(padding)), suffix]) - return os.path.join(root, fname).replace("\\", "/") + ext = os.path.splitext(path)[-1] - def switch(self, container, context): - self.update(container, context) + is_sequence = bool(representation["context"].get("frame")) + # The path is either a single file or sequence in a folder. + if not is_sequence: + filename = path + else: + filename = re.sub(r"(.*)\.(\d+){}$".format(re.escape(ext)), + "\\1.$F4{}".format(ext), + path) + + filename = os.path.join(path, filename) + + filename = os.path.normpath(filename) + filename = filename.replace("\\", "/") + + return filename + + def get_colorspace_parms(self, representation: dict) -> dict: + """Return the color space parameters. + + Returns the values for the colorspace parameters on the node if there + is colorspace data on the representation. + + Arguments: + representation (dict): The representation entity. + + Returns: + dict: Parm to value mapping if colorspace data is defined. + + """ + # Using OCIO colorspace on COP2 File node is only supported in Hou 20+ + major, _, _ = hou.applicationVersion() + if major < 20: + return {} + + data = representation.get("data", {}).get("colorspaceData", {}) + if not data: + return {} + + colorspace = data["colorspace"] + if colorspace: + return { + "colorspace": 3, # Use OpenColorIO + "ocio_space": colorspace + } + + def switch(self, container, representation): + self.update(container, representation) diff --git a/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml b/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml index b93445a974..b6e78cbdc8 100644 --- a/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml +++ b/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml @@ -1,7 +1,7 @@ - + QtWidgets.QAction: """Create AYON menu. @@ -73,7 +73,7 @@ class OpenPypeMenu(object): help_action = None for item in menu_items: if name in item.title(): - # we already have OpenPype menu + # we already have AYON menu return item if before in item.title(): @@ -85,50 +85,50 @@ class OpenPypeMenu(object): self.menu = op_menu return op_menu - def build_openpype_menu(self) -> QtWidgets.QAction: + def _build_ayon_menu(self) -> QtWidgets.QAction: """Build items in AYON menu.""" - openpype_menu = self.get_or_create_openpype_menu() - load_action = QtWidgets.QAction("Load...", openpype_menu) + ayon_menu = self._get_or_create_ayon_menu() + load_action = QtWidgets.QAction("Load...", ayon_menu) load_action.triggered.connect(self.load_callback) - openpype_menu.addAction(load_action) + ayon_menu.addAction(load_action) - publish_action = QtWidgets.QAction("Publish...", openpype_menu) + publish_action = QtWidgets.QAction("Publish...", ayon_menu) publish_action.triggered.connect(self.publish_callback) - openpype_menu.addAction(publish_action) + ayon_menu.addAction(publish_action) - manage_action = QtWidgets.QAction("Manage...", openpype_menu) + manage_action = QtWidgets.QAction("Manage...", ayon_menu) manage_action.triggered.connect(self.manage_callback) - openpype_menu.addAction(manage_action) + ayon_menu.addAction(manage_action) - library_action = QtWidgets.QAction("Library...", openpype_menu) + library_action = QtWidgets.QAction("Library...", ayon_menu) library_action.triggered.connect(self.library_callback) - openpype_menu.addAction(library_action) + ayon_menu.addAction(library_action) - openpype_menu.addSeparator() + ayon_menu.addSeparator() - workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) + workfiles_action = QtWidgets.QAction("Work Files...", ayon_menu) workfiles_action.triggered.connect(self.workfiles_callback) - openpype_menu.addAction(workfiles_action) + ayon_menu.addAction(workfiles_action) - openpype_menu.addSeparator() + ayon_menu.addSeparator() - res_action = QtWidgets.QAction("Set Resolution", openpype_menu) + res_action = QtWidgets.QAction("Set Resolution", ayon_menu) res_action.triggered.connect(self.resolution_callback) - openpype_menu.addAction(res_action) + ayon_menu.addAction(res_action) - frame_action = QtWidgets.QAction("Set Frame Range", openpype_menu) + frame_action = QtWidgets.QAction("Set Frame Range", ayon_menu) frame_action.triggered.connect(self.frame_range_callback) - openpype_menu.addAction(frame_action) + ayon_menu.addAction(frame_action) - colorspace_action = QtWidgets.QAction("Set Colorspace", openpype_menu) + colorspace_action = QtWidgets.QAction("Set Colorspace", ayon_menu) colorspace_action.triggered.connect(self.colorspace_callback) - openpype_menu.addAction(colorspace_action) + ayon_menu.addAction(colorspace_action) - unit_scale_action = QtWidgets.QAction("Set Unit Scale", openpype_menu) + unit_scale_action = QtWidgets.QAction("Set Unit Scale", ayon_menu) unit_scale_action.triggered.connect(self.unit_scale_callback) - openpype_menu.addAction(unit_scale_action) + ayon_menu.addAction(unit_scale_action) - return openpype_menu + return ayon_menu def load_callback(self): """Callback to show Loader tool.""" diff --git a/client/ayon_core/hosts/max/api/pipeline.py b/client/ayon_core/hosts/max/api/pipeline.py index 6fd0a501ff..4b1dcc25d3 100644 --- a/client/ayon_core/hosts/max/api/pipeline.py +++ b/client/ayon_core/hosts/max/api/pipeline.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Pipeline tools for OpenPype Houdini integration.""" +"""Pipeline tools for AYON 3ds max integration.""" import os import logging from operator import attrgetter @@ -14,7 +14,7 @@ from ayon_core.pipeline import ( AVALON_CONTAINER_ID, AYON_CONTAINER_ID, ) -from ayon_core.hosts.max.api.menu import OpenPypeMenu +from ayon_core.hosts.max.api.menu import AYONMenu from ayon_core.hosts.max.api import lib from ayon_core.hosts.max.api.plugin import MS_CUSTOM_ATTRIB from ayon_core.hosts.max import MAX_HOST_DIR @@ -48,7 +48,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_creator_plugin_path(CREATE_PATH) # self._register_callbacks() - self.menu = OpenPypeMenu() + self.menu = AYONMenu() self._has_been_setup = True @@ -94,7 +94,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def _deferred_menu_creation(self): self.log.info("Building menu ...") - self.menu = OpenPypeMenu() + self.menu = AYONMenu() @staticmethod def create_context_node(): @@ -148,7 +148,7 @@ attributes "OpenPypeContext" def ls() -> list: - """Get all OpenPype instances.""" + """Get all AYON containers.""" objs = rt.objects containers = [ obj for obj in objs diff --git a/client/ayon_core/hosts/max/api/plugin.py b/client/ayon_core/hosts/max/api/plugin.py index 4d5d18a42d..e5d12ce87d 100644 --- a/client/ayon_core/hosts/max/api/plugin.py +++ b/client/ayon_core/hosts/max/api/plugin.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""3dsmax specific Avalon/Pyblish plugin definitions.""" +"""3dsmax specific AYON/Pyblish plugin definitions.""" from abc import ABCMeta import six @@ -156,10 +156,6 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" )""" -class OpenPypeCreatorError(CreatorError): - pass - - class MaxCreatorBase(object): @staticmethod diff --git a/client/ayon_core/hosts/max/hooks/force_startup_script.py b/client/ayon_core/hosts/max/hooks/force_startup_script.py index 659be7dfc6..8ccd658e8f 100644 --- a/client/ayon_core/hosts/max/hooks/force_startup_script.py +++ b/client/ayon_core/hosts/max/hooks/force_startup_script.py @@ -6,7 +6,7 @@ from ayon_core.lib.applications import PreLaunchHook, LaunchTypes class ForceStartupScript(PreLaunchHook): - """Inject OpenPype environment to 3ds max. + """Inject AYON environment to 3ds max. Note that this works in combination whit 3dsmax startup script that is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH diff --git a/client/ayon_core/hosts/max/hooks/inject_python.py b/client/ayon_core/hosts/max/hooks/inject_python.py index 36d53551ba..b1b36e75bd 100644 --- a/client/ayon_core/hosts/max/hooks/inject_python.py +++ b/client/ayon_core/hosts/max/hooks/inject_python.py @@ -5,7 +5,7 @@ from ayon_core.lib.applications import PreLaunchHook, LaunchTypes class InjectPythonPath(PreLaunchHook): - """Inject OpenPype environment to 3dsmax. + """Inject AYON environment to 3dsmax. Note that this works in combination whit 3dsmax startup script that is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_mesh_has_uv.py b/client/ayon_core/hosts/max/plugins/publish/validate_mesh_has_uv.py index 109b7fe0b5..ccd91da2be 100644 --- a/client/ayon_core/hosts/max/plugins/publish/validate_mesh_has_uv.py +++ b/client/ayon_core/hosts/max/plugins/publish/validate_mesh_has_uv.py @@ -39,6 +39,8 @@ class ValidateMeshHasUVs(pyblish.api.InstancePlugin, return invalid def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: bullet_point_invalid_statement = "\n".join( diff --git a/client/ayon_core/hosts/max/startup/startup.ms b/client/ayon_core/hosts/max/startup/startup.ms index 4c597901f3..2dfe53a6a5 100644 --- a/client/ayon_core/hosts/max/startup/startup.ms +++ b/client/ayon_core/hosts/max/startup/startup.ms @@ -1,4 +1,4 @@ --- OpenPype Init Script +-- AYON Init Script ( local sysPath = dotNetClass "System.IO.Path" local sysDir = dotNetClass "System.IO.Directory" diff --git a/client/ayon_core/hosts/maya/api/commands.py b/client/ayon_core/hosts/maya/api/commands.py index e63800e542..22cf0871e2 100644 --- a/client/ayon_core/hosts/maya/api/commands.py +++ b/client/ayon_core/hosts/maya/api/commands.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""OpenPype script commands to be used directly in Maya.""" +"""AYON script commands to be used directly in Maya.""" from maya import cmds from ayon_api import get_project, get_folder_by_path diff --git a/client/ayon_core/hosts/maya/api/customize.py b/client/ayon_core/hosts/maya/api/customize.py index da046b538d..4db8819ff5 100644 --- a/client/ayon_core/hosts/maya/api/customize.py +++ b/client/ayon_core/hosts/maya/api/customize.py @@ -109,7 +109,7 @@ def override_toolbox_ui(): controls.append( cmds.iconTextButton( - "pype_toolbox_lookmanager", + "ayon_toolbox_lookmanager", annotation="Look Manager", label="Look Manager", image=os.path.join(icons, "lookmanager.png"), @@ -122,7 +122,7 @@ def override_toolbox_ui(): controls.append( cmds.iconTextButton( - "pype_toolbox_workfiles", + "ayon_toolbox_workfiles", annotation="Work Files", label="Work Files", image=os.path.join(icons, "workfiles.png"), @@ -137,7 +137,7 @@ def override_toolbox_ui(): controls.append( cmds.iconTextButton( - "pype_toolbox_loader", + "ayon_toolbox_loader", annotation="Loader", label="Loader", image=os.path.join(icons, "loader.png"), @@ -152,7 +152,7 @@ def override_toolbox_ui(): controls.append( cmds.iconTextButton( - "pype_toolbox_manager", + "ayon_toolbox_manager", annotation="Inventory", label="Inventory", image=os.path.join(icons, "inventory.png"), diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 7c3c739d7c..8ca898f621 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -2931,13 +2931,13 @@ def bake_to_world_space(nodes, def load_capture_preset(data): - """Convert OpenPype Extract Playblast settings to `capture` arguments + """Convert AYON Extract Playblast settings to `capture` arguments Input data is the settings from: `project_settings/maya/publish/ExtractPlayblast/capture_preset` Args: - data (dict): Capture preset settings from OpenPype settings + data (dict): Capture preset settings from AYON settings Returns: dict: `capture.capture` compatible keyword arguments @@ -3288,7 +3288,7 @@ def set_colorspace(): else: # TODO: deprecated code from 3.15.5 - remove # Maya 2022+ introduces new OCIO v2 color management settings that - # can override the old color management preferences. OpenPype has + # can override the old color management preferences. AYON has # separate settings for both so we fall back when necessary. use_ocio_v2 = imageio["colorManagementPreference_v2"]["enabled"] if use_ocio_v2 and not ocio_v2_support: diff --git a/client/ayon_core/hosts/maya/api/lib_rendersetup.py b/client/ayon_core/hosts/maya/api/lib_rendersetup.py index fb6dd13ce0..c2b5ec843c 100644 --- a/client/ayon_core/hosts/maya/api/lib_rendersetup.py +++ b/client/ayon_core/hosts/maya/api/lib_rendersetup.py @@ -3,7 +3,7 @@ https://github.com/Colorbleed/colorbleed-config/blob/acre/colorbleed/maya/lib_rendersetup.py Credits: Roy Nieterau (BigRoy) / Colorbleed -Modified for use in OpenPype +Modified for use in AYON """ diff --git a/client/ayon_core/hosts/maya/api/menu.py b/client/ayon_core/hosts/maya/api/menu.py index 8de025bfe5..0cb7edd40d 100644 --- a/client/ayon_core/hosts/maya/api/menu.py +++ b/client/ayon_core/hosts/maya/api/menu.py @@ -50,7 +50,7 @@ def get_context_label(): def install(project_settings): if cmds.about(batch=True): - log.info("Skipping openpype.menu initialization in batch mode..") + log.info("Skipping AYON menu initialization in batch mode..") return def add_menu(): @@ -261,7 +261,7 @@ def popup(): def update_menu_task_label(): - """Update the task label in Avalon menu to current session""" + """Update the task label in AYON menu to current session""" if IS_HEADLESS: return diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index 9792a4a5fe..b3e401b91e 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -361,13 +361,13 @@ def parse_container(container): def _ls(): - """Yields Avalon container node names. + """Yields AYON container node names. Used by `ls()` to retrieve the nodes and then query the full container's data. Yields: - str: Avalon container node name (objectSet) + str: AYON container node name (objectSet) """ @@ -384,7 +384,7 @@ def _ls(): } # Iterate over all 'set' nodes in the scene to detect whether - # they have the avalon container ".id" attribute. + # they have the ayon container ".id" attribute. fn_dep = om.MFnDependencyNode() iterator = om.MItDependencyNodes(om.MFn.kSet) for mobject in _maya_iterate(iterator): @@ -673,7 +673,7 @@ def workfile_save_before_xgen(event): switching context. Args: - event (Event) - openpype/lib/events.py + event (Event) - ayon_core/lib/events.py """ if not cmds.pluginInfo("xgenToolkit", query=True, loaded=True): return diff --git a/client/ayon_core/hosts/maya/api/plugin.py b/client/ayon_core/hosts/maya/api/plugin.py index bdb0cb1c99..6f8b74c906 100644 --- a/client/ayon_core/hosts/maya/api/plugin.py +++ b/client/ayon_core/hosts/maya/api/plugin.py @@ -899,7 +899,7 @@ class ReferenceLoader(Loader): cmds.disconnectAttr(input, node_attr) cmds.setAttr(node_attr, data["value"]) - # Fix PLN-40 for older containers created with Avalon that had the + # Fix PLN-40 for older containers created with AYON that had the # `.verticesOnlySet` set to True. if cmds.getAttr("{}.verticesOnlySet".format(node)): self.log.info("Setting %s.verticesOnlySet to False", node) diff --git a/client/ayon_core/hosts/maya/api/render_setup_tools.py b/client/ayon_core/hosts/maya/api/render_setup_tools.py index a6b46e1e9a..a5e04de184 100644 --- a/client/ayon_core/hosts/maya/api/render_setup_tools.py +++ b/client/ayon_core/hosts/maya/api/render_setup_tools.py @@ -5,7 +5,7 @@ Export Maya nodes from Render Setup layer as if flattened in that layer instead of exporting the defaultRenderLayer as Maya forces by default Credits: Roy Nieterau (BigRoy) / Colorbleed -Modified for use in OpenPype +Modified for use in AYON """ diff --git a/client/ayon_core/hosts/maya/api/setdress.py b/client/ayon_core/hosts/maya/api/setdress.py index 7276ae254c..b1d5beb343 100644 --- a/client/ayon_core/hosts/maya/api/setdress.py +++ b/client/ayon_core/hosts/maya/api/setdress.py @@ -150,7 +150,7 @@ def load_package(filepath, name, namespace=None): containers.append(container) # TODO: Do we want to cripple? Or do we want to add a 'parent' parameter? - # Cripple the original avalon containers so they don't show up in the + # Cripple the original AYON containers so they don't show up in the # manager # for container in containers: # cmds.setAttr("%s.id" % container, @@ -175,7 +175,7 @@ def _add(instance, representation_id, loaders, namespace, root="|"): namespace (str): Returns: - str: The created Avalon container. + str: The created AYON container. """ @@ -244,7 +244,7 @@ def _instances_by_namespace(data): def get_contained_containers(container): - """Get the Avalon containers in this container + """Get the AYON containers in this container Args: container (dict): The container dict. @@ -256,7 +256,7 @@ def get_contained_containers(container): from .pipeline import parse_container - # Get avalon containers in this package setdress container + # Get AYON containers in this package setdress container containers = [] members = cmds.sets(container['objectName'], query=True) for node in cmds.ls(members, type="objectSet"): diff --git a/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py b/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py index 9907b5f340..685602ef0b 100644 --- a/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py +++ b/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py @@ -19,7 +19,7 @@ class MayaLegacyConvertor(ProductConvertorPlugin, Its limitation is that you can have multiple creators creating product of the same type and there is no way to handle it. This code should - nevertheless cover all creators that came with OpenPype. + nevertheless cover all creators that came with AYON. """ identifier = "io.openpype.creators.maya.legacy" diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py index eaa7ff1ae3..f0fb89e5a4 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -108,7 +108,7 @@ class LoadVDBtoArnold(load.LoaderPlugin): from maya import cmds - # Get all members of the avalon container, ensure they are unlocked + # Get all members of the AYON container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py index 1707008b67..cad0900590 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -115,7 +115,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin): def remove(self, container): from maya import cmds - # Get all members of the avalon container, ensure they are unlocked + # Get all members of the AYON container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py index 42d4583d76..88f62e81a4 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -277,7 +277,7 @@ class LoadVDBtoVRay(load.LoaderPlugin): def remove(self, container): - # Get all members of the avalon container, ensure they are unlocked + # Get all members of the AYON container, ensure they are unlocked # and delete everything members = cmds.sets(container['objectName'], query=True) cmds.lockNode(members, lock=False) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py b/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py index 651197627e..59d8eadefa 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py @@ -7,9 +7,9 @@ loader will use them instead of native vray vrmesh format. """ import os -from ayon_api import get_representation_by_name import maya.cmds as cmds +import ayon_api from ayon_core.settings import get_project_settings from ayon_core.pipeline import ( load, diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_inputs.py b/client/ayon_core/hosts/maya/plugins/publish/collect_inputs.py index d084804e05..fa5a694a76 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_inputs.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_inputs.py @@ -79,12 +79,12 @@ def iter_history(nodes, def collect_input_containers(containers, nodes): """Collect containers that contain any of the node in `nodes`. - This will return any loaded Avalon container that contains at least one of - the nodes. As such, the Avalon container is an input for it. Or in short, + This will return any loaded AYON container that contains at least one of + the nodes. As such, the AYON container is an input for it. Or in short, there are member nodes of that container. Returns: - list: Input avalon containers + list: Input loaded containers """ # Assume the containers have collected their cached '_members' data diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py b/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py index 31c0d0eaa1..83e743c92e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_multiverse_look.py @@ -40,9 +40,11 @@ class _NodeTypeAttrib(object): return "{}.{}".format(node, self.colour_space) def __str__(self): - return "_NodeTypeAttrib(name={}, fname={}, " - "computed_fname={}, colour_space={})".format( - self.name, self.fname, self.computed_fname, self.colour_space) + return ( + "_NodeTypeAttrib(name={}, fname={}, " + "computed_fname={}, colour_space={})".format( + self.name, self.fname, self.computed_fname, self.colour_space) + ) NODETYPES = { diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_look.py b/client/ayon_core/hosts/maya/plugins/publish/extract_look.py index fa74dd18d3..2a86b20131 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_look.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_look.py @@ -106,10 +106,10 @@ class TextureProcessor: self.log = log def apply_settings(self, project_settings): - """Apply OpenPype system/project settings to the TextureProcessor + """Apply AYON system/project settings to the TextureProcessor Args: - project_settings (dict): OpenPype project settings + project_settings (dict): AYON project settings Returns: None @@ -278,7 +278,7 @@ class MakeTX(TextureProcessor): """Process the texture. This function requires the `maketx` executable to be available in an - OpenImageIO toolset detectable by OpenPype. + OpenImageIO toolset detectable by AYON. Args: source (str): Path to source file. diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py index 9aaba532b2..d305b8dc6c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_workfile_xgen.py @@ -128,9 +128,11 @@ class ExtractWorkfileXgen(publish.Extractor): alembic_files.append(alembic_file) template_data = copy.deepcopy(instance.data["anatomyData"]) - published_maya_path = StringTemplate( - instance.context.data["anatomy"].templates["publish"]["file"] - ).format(template_data) + anatomy = instance.context.data["anatomy"] + publish_template = anatomy.get_template_item( + "publish", "default", "file" + ) + published_maya_path = publish_template.format(template_data) published_basename, _ = os.path.splitext(published_maya_path) for source in alembic_files: diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py b/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py index ee864bd89b..73668da28d 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_xgen.py @@ -39,8 +39,9 @@ class ExtractXgen(publish.Extractor): # Get published xgen file name. template_data = copy.deepcopy(instance.data["anatomyData"]) template_data.update({"ext": "xgen"}) - templates = instance.context.data["anatomy"].templates["publish"] - xgen_filename = StringTemplate(templates["file"]).format(template_data) + anatomy = instance.context.data["anatomy"] + file_template = anatomy.get_template_item("publish", "default", "file") + xgen_filename = file_template.format(template_data) xgen_path = os.path.join( self.staging_dir(instance), xgen_filename diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py index b7f115b38f..2ba2bff6fc 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animated_reference.py @@ -2,12 +2,14 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( PublishValidationError, - ValidateContentsOrder + ValidateContentsOrder, + OptionalPyblishPluginMixin ) from maya import cmds -class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): +class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate all nodes in skeletonAnim_SET are referenced""" order = ValidateContentsOrder @@ -16,8 +18,11 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): label = "Animated Reference Rig" accepted_controllers = ["transform", "locator"] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False def process(self, instance): + if not self.is_active(instance.data): + return animated_sets = instance.data.get("animated_skeleton", []) if not animated_sets: self.log.debug( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_content.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_content.py index 8cf5c4278e..ea989bbcf3 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_content.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_content.py @@ -2,11 +2,13 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( PublishValidationError, - ValidateContentsOrder + ValidateContentsOrder, + OptionalPyblishPluginMixin ) -class ValidateAnimationContent(pyblish.api.InstancePlugin): +class ValidateAnimationContent(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Adheres to the content of 'animation' product type - Must have collected `out_hierarchy` data. @@ -19,6 +21,7 @@ class ValidateAnimationContent(pyblish.api.InstancePlugin): families = ["animation"] label = "Animation Content" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False @classmethod def get_invalid(cls, instance): @@ -48,6 +51,8 @@ class ValidateAnimationContent(pyblish.api.InstancePlugin): return invalid def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py index 0adb0a201c..b96c5f07e2 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_animation_out_set_related_node_ids.py @@ -6,11 +6,13 @@ from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin): +class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate if deformed shapes have related IDs to the original shapes When a deformer is applied in the scene on a referenced mesh that already @@ -28,10 +30,12 @@ class ValidateOutRelatedNodeIds(pyblish.api.InstancePlugin): ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction ] + optional = False def process(self, instance): """Process all meshes""" - + if not self.is_active(instance.data): + return # Ensure all nodes have a cbId and a related ID to the original shapes # if a deformer has been created on the shape invalid = self.get_invalid(instance) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/client/ayon_core/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index f50fa1ed41..8bcd272d01 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -1,11 +1,15 @@ import pyblish.api from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( - ValidateContentsOrder, PublishValidationError, RepairAction + ValidateContentsOrder, + PublishValidationError, + RepairAction, + OptionalPyblishPluginMixin ) -class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): +class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate Arnold Scene Source Cbid. It is required for the proxy and content nodes to share the same cbid. @@ -16,6 +20,7 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): families = ["ass"] label = "Validate Arnold Scene Source CBID" actions = [RepairAction] + optional = False @staticmethod def _get_nodes_by_name(nodes): @@ -55,6 +60,8 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): return invalid_couples def process(self, instance): + if not self.is_active(instance.data): + return # Proxy validation. if not instance.data.get("proxy", []): return diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_ass_relative_paths.py b/client/ayon_core/hosts/maya/plugins/publish/validate_ass_relative_paths.py index 669708d3a6..6e65eee592 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_ass_relative_paths.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_ass_relative_paths.py @@ -8,11 +8,13 @@ import pyblish.api from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateAssRelativePaths(pyblish.api.InstancePlugin): +class ValidateAssRelativePaths(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure exporting ass file has set relative texture paths""" order = ValidateContentsOrder @@ -20,8 +22,11 @@ class ValidateAssRelativePaths(pyblish.api.InstancePlugin): families = ['ass'] label = "ASS has relative texture paths" actions = [RepairAction] + optional = False def process(self, instance): + if not self.is_active(instance.data): + return # we cannot ask this until user open render settings as # `defaultArnoldRenderOptions` doesn't exist errors = [] diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_name.py b/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_name.py index 03fa0fd779..c829f4bf74 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_name.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_name.py @@ -2,11 +2,13 @@ import pyblish.api import maya.cmds as cmds import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateAssemblyName(pyblish.api.InstancePlugin): +class ValidateAssemblyName(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """ Ensure Assembly name ends with `GRP` Check if assembly name ends with `_GRP` string. @@ -17,6 +19,7 @@ class ValidateAssemblyName(pyblish.api.InstancePlugin): families = ["assembly"] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] active = False + optional = True @classmethod def get_invalid(cls, instance): @@ -47,7 +50,8 @@ class ValidateAssemblyName(pyblish.api.InstancePlugin): return invalid def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Found {} invalid named assembly " diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_namespaces.py b/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_namespaces.py index 2d3d8e71ac..814a8295c4 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_namespaces.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_namespaces.py @@ -1,10 +1,12 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin): +class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure namespaces are not nested In the outliner an item in a normal namespace looks as following: @@ -20,9 +22,11 @@ class ValidateAssemblyNamespaces(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder families = ["assembly"] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False def process(self, instance): - + if not self.is_active(instance.data): + return self.log.debug("Checking namespace for %s" % instance.name) if self.get_invalid(instance): raise PublishValidationError("Nested namespaces found") diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_transforms.py b/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_transforms.py index 5069feb4b6..3bcae5de49 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_transforms.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_assembly_transforms.py @@ -2,10 +2,15 @@ import pyblish.api from maya import cmds import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import PublishValidationError, RepairAction +from ayon_core.pipeline.publish import ( + PublishValidationError, + RepairAction, + OptionalPyblishPluginMixin +) -class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin): +class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Verify only root nodes of the loaded asset have transformations. Note: This check is temporary and is subject to change. @@ -34,7 +39,11 @@ class ValidateAssemblyModelTransforms(pyblish.api.InstancePlugin): " This can alter the look of your scene. " "Are you sure you want to continue?") + optional = False + def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_camera_attributes.py b/client/ayon_core/hosts/maya/plugins/publish/validate_camera_attributes.py index 5e940a48a9..5fd8772a96 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_camera_attributes.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_camera_attributes.py @@ -3,10 +3,14 @@ from maya import cmds import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( - PublishValidationError, ValidateContentsOrder) + PublishValidationError, + ValidateContentsOrder, + OptionalPyblishPluginMixin +) -class ValidateCameraAttributes(pyblish.api.InstancePlugin): +class ValidateCameraAttributes(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates Camera has no invalid attribute keys or values. The Alembic file format does not a specific subset of attributes as such @@ -20,6 +24,7 @@ class ValidateCameraAttributes(pyblish.api.InstancePlugin): hosts = ['maya'] label = 'Camera Attributes' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = True DEFAULTS = [ ("filmFitOffset", 0.0), @@ -62,7 +67,8 @@ class ValidateCameraAttributes(pyblish.api.InstancePlugin): def process(self, instance): """Process all the nodes in the instance""" - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_camera_contents.py b/client/ayon_core/hosts/maya/plugins/publish/validate_camera_contents.py index 7d4c4341fd..0f14a057f9 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_camera_contents.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_camera_contents.py @@ -3,10 +3,13 @@ from maya import cmds import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( - PublishValidationError, ValidateContentsOrder) + PublishValidationError, + ValidateContentsOrder, + OptionalPyblishPluginMixin) -class ValidateCameraContents(pyblish.api.InstancePlugin): +class ValidateCameraContents(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates Camera instance contents. A Camera instance may only hold a SINGLE camera's transform, nothing else. @@ -22,6 +25,7 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): label = 'Camera Contents' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] validate_shapes = True + optional = False @classmethod def get_invalid(cls, instance): @@ -71,7 +75,8 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): def process(self, instance): """Process all the nodes in the instance""" - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Invalid camera contents: " diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py index 55c4973842..4590c53931 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_current_renderlayer_renderable.py @@ -1,10 +1,13 @@ import pyblish.api from maya import cmds -from ayon_core.pipeline.publish import context_plugin_should_run +from ayon_core.pipeline.publish import ( + context_plugin_should_run, + OptionalPyblishPluginMixin +) - -class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin): +class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin, + OptionalPyblishPluginMixin): """Validate if current render layer has a renderable camera There is a bug in Redshift which occurs when the current render layer @@ -20,9 +23,11 @@ class ValidateCurrentRenderLayerIsRenderable(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder hosts = ["maya"] families = ["renderlayer"] + optional = False def process(self, context): - + if not self.is_active(context.data): + return # Workaround bug pyblish-base#250 if not context_plugin_should_run(self, context): return diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_glsl_material.py b/client/ayon_core/hosts/maya/plugins/publish/validate_glsl_material.py index e610a8118c..3735dbb74c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_glsl_material.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_glsl_material.py @@ -6,10 +6,11 @@ from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder ) -from ayon_core.pipeline import PublishValidationError +from ayon_core.pipeline import PublishValidationError, OptionalPyblishPluginMixin -class ValidateGLSLMaterial(pyblish.api.InstancePlugin): +class ValidateGLSLMaterial(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """ Validate if the asset uses GLSL Shader """ @@ -23,6 +24,8 @@ class ValidateGLSLMaterial(pyblish.api.InstancePlugin): active = True def process(self, instance): + if not self.is_active(instance.data): + return shading_grp = self.get_material_from_shapes(instance) if not shading_grp: raise PublishValidationError("No shading group found") diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_glsl_plugin.py b/client/ayon_core/hosts/maya/plugins/publish/validate_glsl_plugin.py index e155315e4f..d783da8b5c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_glsl_plugin.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_glsl_plugin.py @@ -5,11 +5,13 @@ import pyblish.api from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateGLSLPlugin(pyblish.api.InstancePlugin): +class ValidateGLSLPlugin(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """ Validate if the asset uses GLSL Shader """ @@ -19,8 +21,11 @@ class ValidateGLSLPlugin(pyblish.api.InstancePlugin): hosts = ['maya'] label = 'maya2glTF plugin' actions = [RepairAction] + optional = False def process(self, instance): + if not self.is_active(instance.data): + return if not cmds.pluginInfo("maya2glTF", query=True, loaded=True): raise PublishValidationError("maya2glTF is not loaded") diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_content.py b/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_content.py deleted file mode 100644 index 5f57b31868..0000000000 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_content.py +++ /dev/null @@ -1,75 +0,0 @@ -import maya.cmds as cmds -import pyblish.api - -from ayon_core.hosts.maya.api import lib -from ayon_core.pipeline.publish import PublishValidationError - - -class ValidateInstancerContent(pyblish.api.InstancePlugin): - """Validates that all meshes in the instance have object IDs. - - This skips a check on intermediate objects because we consider them - not important. - """ - order = pyblish.api.ValidatorOrder - label = 'Instancer Content' - families = ['instancer'] - - def process(self, instance): - - error = False - members = instance.data['setMembers'] - export_members = instance.data['exactExportMembers'] - - self.log.debug("Contents {0}".format(members)) - - if not len(members) == len(cmds.ls(members, type="instancer")): - self.log.error("Instancer can only contain instancers") - error = True - - # TODO: Implement better check for particles are cached - if not cmds.ls(export_members, type="nucleus"): - self.log.error("Instancer must have a connected nucleus") - error = True - - if not cmds.ls(export_members, type="cacheFile"): - self.log.error("Instancer must be cached") - error = True - - hidden = self.check_geometry_hidden(export_members) - if not hidden: - error = True - self.log.error("Instancer input geometry must be hidden " - "the scene. Invalid: {0}".format(hidden)) - - # Ensure all in one group - parents = cmds.listRelatives(members, - allParents=True, - fullPath=True) or [] - roots = list(set(cmds.ls(parents, assemblies=True, long=True))) - if len(roots) > 1: - self.log.error("Instancer should all be contained in a single " - "group. Current roots: {0}".format(roots)) - error = True - - if error: - raise PublishValidationError( - "Instancer Content is invalid. See log.") - - def check_geometry_hidden(self, export_members): - - # Ensure all instanced geometry is hidden - shapes = cmds.ls(export_members, - dag=True, - shapes=True, - noIntermediate=True) - meshes = cmds.ls(shapes, type="mesh") - - visible = [node for node in meshes - if lib.is_visible(node, - displayLayer=False, - intermediateObject=False)] - if visible: - return False - - return True diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py b/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py deleted file mode 100644 index be6724d7e9..0000000000 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_instancer_frame_ranges.py +++ /dev/null @@ -1,167 +0,0 @@ -import os -import re - -import pyblish.api - -from ayon_core.pipeline.publish import PublishValidationError - - -def is_cache_resource(resource): - """Return whether resource is a cacheFile resource""" - required = set(["maya", "node", "cacheFile"]) - tags = resource.get("tags", []) - return required.issubset(tags) - - -def valdidate_files(files): - for f in files: - assert os.path.exists(f) - assert f.endswith(".mcx") or f.endswith(".mcc") - - return True - - -def filter_ticks(files): - tick_files = set() - ticks = set() - for path in files: - match = re.match(".+Tick([0-9]+).mcx$", os.path.basename(path)) - if match: - tick_files.add(path) - num = match.group(1) - ticks.add(int(num)) - - return tick_files, ticks - - -class ValidateInstancerFrameRanges(pyblish.api.InstancePlugin): - """Validates all instancer particle systems are cached correctly. - - This means they should have the files/frames as required by the start-end - frame (including handles). - - This also checks the files exist and checks the "ticks" (substeps) files. - - """ - order = pyblish.api.ValidatorOrder - label = 'Instancer Cache Frame Ranges' - families = ['instancer'] - - @classmethod - def get_invalid(cls, instance): - - import pyseq - - start_frame = instance.data.get("frameStart", 0) - end_frame = instance.data.get("frameEnd", 0) - required = range(int(start_frame), int(end_frame) + 1) - - invalid = list() - resources = instance.data.get("resources", []) - - for resource in resources: - if not is_cache_resource(resource): - continue - - node = resource['node'] - all_files = resource['files'][:] - all_lookup = set(all_files) - - # The first file is usually the .xml description file. - xml = all_files.pop(0) - assert xml.endswith(".xml") - - # Ensure all files exist (including ticks) - # The remainder file paths should be the .mcx or .mcc files - valdidate_files(all_files) - - # Maya particle caches support substeps by saving out additional - # files that end with a Tick60.mcx, Tick120.mcx, etc. suffix. - # To avoid `pyseq` getting confused we filter those out and then - # for each file (except the last frame) check that at least all - # ticks exist. - - tick_files, ticks = filter_ticks(all_files) - if tick_files: - files = [f for f in all_files if f not in tick_files] - else: - files = all_files - - sequences = pyseq.get_sequences(files) - if len(sequences) != 1: - invalid.append(node) - cls.log.warning("More than one sequence found? " - "{0} {1}".format(node, files)) - cls.log.warning("Found caches: {0}".format(sequences)) - continue - - sequence = sequences[0] - cls.log.debug("Found sequence: {0}".format(sequence)) - - start = sequence.start() - end = sequence.end() - - if start > start_frame or end < end_frame: - invalid.append(node) - cls.log.warning("Sequence does not have enough " - "frames: {0}-{1} (requires: {2}-{3})" - "".format(start, end, - start_frame, - end_frame)) - continue - - # Ensure all frames are present - missing = set(sequence.missing()) - if missing: - required_missing = [x for x in required if x in missing] - if required_missing: - invalid.append(node) - cls.log.warning("Sequence is missing required frames: " - "{0}".format(required_missing)) - continue - - # Ensure all tick files (substep) exist for the files in the folder - # for the frames required by the time range. - if ticks: - ticks = list(sorted(ticks)) - cls.log.debug("Found ticks: {0} " - "(substeps: {1})".format(ticks, len(ticks))) - - # Check all frames except the last since we don't - # require subframes after our time range. - tick_check_frames = set(required[:-1]) - - # Check all frames - for item in sequence: - frame = item.frame - if not frame: - invalid.append(node) - cls.log.error("Path is not a frame in sequence: " - "{0}".format(item)) - continue - - # Not required for our time range - if frame not in tick_check_frames: - continue - - path = item.path - for num in ticks: - base, ext = os.path.splitext(path) - tick_file = base + "Tick{0}".format(num) + ext - if tick_file not in all_lookup: - invalid.append(node) - cls.log.warning("Tick file found that is not " - "in cache query filenames: " - "{0}".format(tick_file)) - - return invalid - - def process(self, instance): - - invalid = self.get_invalid(instance) - - if invalid: - self.log.error("Invalid nodes: {0}".format(invalid)) - raise PublishValidationError( - ("Invalid particle caches in instance. " - "See logs for details.")) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_loaded_plugin.py b/client/ayon_core/hosts/maya/plugins/publish/validate_loaded_plugin.py index 54a3e16111..a05920a21e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_loaded_plugin.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_loaded_plugin.py @@ -4,24 +4,27 @@ import maya.cmds as cmds from ayon_core.pipeline.publish import ( RepairContextAction, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateLoadedPlugin(pyblish.api.ContextPlugin): +class ValidateLoadedPlugin(pyblish.api.ContextPlugin, + OptionalPyblishPluginMixin): """Ensure there are no unauthorized loaded plugins""" label = "Loaded Plugin" order = pyblish.api.ValidatorOrder host = ["maya"] actions = [RepairContextAction] + optional = True @classmethod def get_invalid(cls, context): invalid = [] loaded_plugin = cmds.pluginInfo(query=True, listPlugins=True) - # get variable from OpenPype settings + # get variable from AYON settings whitelist_native_plugins = cls.whitelist_native_plugins authorized_plugins = cls.authorized_plugins or [] @@ -35,7 +38,8 @@ class ValidateLoadedPlugin(pyblish.api.ContextPlugin): return invalid def process(self, context): - + if not self.is_active(context.data): + return invalid = self.get_invalid(context) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py b/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py index 656b91216b..e70a805de4 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_look_shading_group.py @@ -5,11 +5,13 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateShadingEngine(pyblish.api.InstancePlugin): +class ValidateShadingEngine(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate all shading engines are named after the surface material. Shading engines should be named "{surface_shader}SG" @@ -22,9 +24,12 @@ class ValidateShadingEngine(pyblish.api.InstancePlugin): actions = [ ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction ] + optional = True # The default connections to check def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_maya_units.py b/client/ayon_core/hosts/maya/plugins/publish/validate_maya_units.py index 1c00d86868..f1c171bddc 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_maya_units.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_maya_units.py @@ -7,11 +7,13 @@ from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.pipeline.publish import ( RepairContextAction, ValidateSceneOrder, - PublishXmlValidationError + PublishXmlValidationError, + OptionalPyblishPluginMixin ) -class ValidateMayaUnits(pyblish.api.ContextPlugin): +class ValidateMayaUnits(pyblish.api.ContextPlugin, + OptionalPyblishPluginMixin): """Check if the Maya units are set correct""" order = ValidateSceneOrder @@ -35,6 +37,7 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): "Maya scene {setting} must be '{required_value}'. " "Current value is '{current_value}'." ) + optional = False @classmethod def apply_settings(cls, project_settings): @@ -52,7 +55,8 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): cls.validate_fps = settings.get("validate_fps", cls.validate_fps) def process(self, context): - + if not self.is_active(context.data): + return # Collected units linearunits = context.data.get('linearUnits') angularunits = context.data.get('angularUnits') diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py index e76553629f..8f80b689fd 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_lamina_faces.py @@ -2,10 +2,11 @@ from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import ValidateMeshOrder +from ayon_core.pipeline.publish import ValidateMeshOrder, OptionalPyblishPluginMixin -class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin): +class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate meshes don't have lamina faces. Lamina faces share all of their edges. @@ -17,6 +18,7 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin): families = ['model'] label = 'Mesh Lamina Faces' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = True @staticmethod def get_invalid(instance): @@ -28,6 +30,8 @@ class ValidateMeshLaminaFaces(pyblish.api.InstancePlugin): def process(self, instance): """Process all the nodes in the instance 'objectSet'""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py index f8dfe65b32..5f107b7f7e 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_ngons.py @@ -3,10 +3,14 @@ from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.hosts.maya.api import lib -from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin +) -class ValidateMeshNgons(pyblish.api.Validator): +class ValidateMeshNgons(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Ensure that meshes don't have ngons Ngon are faces with more than 4 sides. @@ -21,6 +25,7 @@ class ValidateMeshNgons(pyblish.api.Validator): families = ["model"] label = "Mesh ngons" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = True @staticmethod def get_invalid(instance): @@ -39,6 +44,8 @@ class ValidateMeshNgons(pyblish.api.Validator): def process(self, instance): """Process all the nodes in the instance "objectSet""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py index 0e9147d978..ff1dca87cf 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_no_negative_scale.py @@ -4,7 +4,8 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateMeshOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) @@ -15,7 +16,8 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateMeshNoNegativeScale(pyblish.api.Validator): +class ValidateMeshNoNegativeScale(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Ensure that meshes don't have a negative scale. Using negatively scaled proxies in a VRayMesh results in inverted @@ -32,6 +34,7 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator): families = ['model'] label = 'Mesh No Negative Scale' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False @staticmethod def get_invalid(instance): @@ -52,7 +55,8 @@ class ValidateMeshNoNegativeScale(pyblish.api.Validator): def process(self, instance): """Process all the nodes in the instance 'objectSet'""" - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py index 1c7ea10a50..6dbad538ef 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_non_manifold.py @@ -4,7 +4,8 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateMeshOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) @@ -15,7 +16,8 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateMeshNonManifold(pyblish.api.Validator): +class ValidateMeshNonManifold(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Ensure that meshes don't have non-manifold edges or vertices To debug the problem on the meshes you can use Maya's modeling @@ -28,6 +30,7 @@ class ValidateMeshNonManifold(pyblish.api.Validator): families = ['model'] label = 'Mesh Non-Manifold Edges/Vertices' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = True @staticmethod def get_invalid(instance): @@ -44,7 +47,8 @@ class ValidateMeshNonManifold(pyblish.api.Validator): def process(self, instance): """Process all the nodes in the instance 'objectSet'""" - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_shader_connections.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_shader_connections.py index d55b58cd0d..8672ac13dd 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_shader_connections.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_shader_connections.py @@ -5,7 +5,8 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, ValidateMeshOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) @@ -79,7 +80,8 @@ def disconnect(node_a, node_b): cmds.disconnectAttr(source, input) -class ValidateMeshShaderConnections(pyblish.api.InstancePlugin): +class ValidateMeshShaderConnections(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure mesh shading engine connections are valid. In some scenarios Maya keeps connections to multiple shaders even if just @@ -96,10 +98,12 @@ class ValidateMeshShaderConnections(pyblish.api.InstancePlugin): label = "Mesh Shader Connections" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction] + optional = True def process(self, instance): """Process all the nodes in the instance 'objectSet'""" - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py index 10b5d77cf3..f0962148dc 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_mesh_vertices_have_edges.py @@ -4,10 +4,15 @@ from maya import cmds import ayon_core.hosts.maya.api.action from ayon_core.hosts.maya.api.lib import len_flattened from ayon_core.pipeline.publish import ( - PublishValidationError, RepairAction, ValidateMeshOrder) + PublishValidationError, + RepairAction, + ValidateMeshOrder, + OptionalPyblishPluginMixin +) -class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin): +class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate meshes have only vertices that are connected to edges. Maya can have invalid geometry with vertices that have no edges or @@ -32,6 +37,7 @@ class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin): label = 'Mesh Vertices Have Edges' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction] + optional = True @classmethod def repair(cls, instance): @@ -72,7 +78,8 @@ class ValidateMeshVerticesHaveEdges(pyblish.api.InstancePlugin): return invalid def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py b/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py index b0db5e435a..37c78a72ee 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_model_content.py @@ -5,11 +5,13 @@ import ayon_core.hosts.maya.api.action from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateModelContent(pyblish.api.InstancePlugin): +class ValidateModelContent(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Adheres to the content of 'model' product type - Must have one top group. (configurable) @@ -24,6 +26,7 @@ class ValidateModelContent(pyblish.api.InstancePlugin): actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] validate_top_group = True + optional = False @classmethod def get_invalid(cls, instance): @@ -91,7 +94,8 @@ class ValidateModelContent(pyblish.api.InstancePlugin): return list(invalid) def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_default_camera.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_default_camera.py index 9977562ca3..3e21ec6e50 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_default_camera.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_default_camera.py @@ -4,7 +4,8 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) @@ -15,7 +16,8 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateNoDefaultCameras(pyblish.api.InstancePlugin): +class ValidateNoDefaultCameras(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure no default (startup) cameras are in the instance. This might be unnecessary. In the past there were some issues with @@ -28,6 +30,7 @@ class ValidateNoDefaultCameras(pyblish.api.InstancePlugin): families = ['camera'] label = "No Default Cameras" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False @staticmethod def get_invalid(instance): @@ -37,6 +40,8 @@ class ValidateNoDefaultCameras(pyblish.api.InstancePlugin): def process(self, instance): """Process all the cameras in the instance""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_namespace.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_namespace.py index b9b8aa2708..7ea2a79339 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_namespace.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_namespace.py @@ -4,7 +4,8 @@ import pyblish.api from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) import ayon_core.hosts.maya.api.action @@ -24,7 +25,8 @@ def get_namespace(node_name): return node_name.rpartition(":")[0] -class ValidateNoNamespace(pyblish.api.InstancePlugin): +class ValidateNoNamespace(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure the nodes don't have a namespace""" order = ValidateContentsOrder @@ -33,6 +35,7 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin): label = 'No Namespaces' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction] + optional = False @staticmethod def get_invalid(instance): @@ -41,6 +44,8 @@ class ValidateNoNamespace(pyblish.api.InstancePlugin): def process(self, instance): """Process all the nodes in the instance""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py index 9899768dc0..a9dc1d5bef 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_null_transforms.py @@ -5,7 +5,8 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) @@ -37,7 +38,8 @@ def has_shape_children(node): return True -class ValidateNoNullTransforms(pyblish.api.InstancePlugin): +class ValidateNoNullTransforms(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure no null transforms are in the scene. Warning: @@ -54,6 +56,7 @@ class ValidateNoNullTransforms(pyblish.api.InstancePlugin): label = 'No Empty/Null Transforms' actions = [RepairAction, ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False @staticmethod def get_invalid(instance): @@ -70,6 +73,8 @@ class ValidateNoNullTransforms(pyblish.api.InstancePlugin): def process(self, instance): """Process all the transform nodes in the instance """ + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_no_vraymesh.py b/client/ayon_core/hosts/maya/plugins/publish/validate_no_vraymesh.py index be8296a820..2d59608e11 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_no_vraymesh.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_no_vraymesh.py @@ -1,7 +1,9 @@ import pyblish.api from maya import cmds -from ayon_core.pipeline.publish import PublishValidationError - +from ayon_core.pipeline.publish import ( + PublishValidationError, + OptionalPyblishPluginMixin +) def _as_report_list(values, prefix="- ", suffix="\n"): """Return list as bullet point list for a report""" @@ -10,15 +12,18 @@ def _as_report_list(values, prefix="- ", suffix="\n"): return prefix + (suffix + prefix).join(values) -class ValidateNoVRayMesh(pyblish.api.InstancePlugin): +class ValidateNoVRayMesh(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate there are no VRayMesh objects in the instance""" order = pyblish.api.ValidatorOrder label = 'No V-Ray Proxies (VRayMesh)' families = ["pointcache"] + optional = False def process(self, instance): - + if not self.is_active(instance.data): + return if not cmds.pluginInfo("vrayformaya", query=True, loaded=True): return diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py index 297618fd4f..73701f8d83 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_no_ghosting.py @@ -3,10 +3,13 @@ from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin - -class ValidateNodeNoGhosting(pyblish.api.InstancePlugin): +) +class ValidateNodeNoGhosting(pyblish.api.InstancePlugin. + OptionalPyblishPluginMixin): """Ensure nodes do not have ghosting enabled. If one would publish towards a non-Maya format it's likely that stats @@ -23,6 +26,7 @@ class ValidateNodeNoGhosting(pyblish.api.InstancePlugin): families = ['model', 'rig'] label = "No Ghosting" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False _attributes = {'ghosting': 0} @@ -46,7 +50,8 @@ class ValidateNodeNoGhosting(pyblish.api.InstancePlugin): return invalid def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_plugin_path_attributes.py b/client/ayon_core/hosts/maya/plugins/publish/validate_plugin_path_attributes.py index fd71039e30..f961ec6e4a 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_plugin_path_attributes.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_plugin_path_attributes.py @@ -8,11 +8,13 @@ from ayon_core.hosts.maya.api.lib import pairwise from ayon_core.hosts.maya.api.action import SelectInvalidAction from ayon_core.pipeline.publish import ( ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): +class ValidatePluginPathAttributes(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """ Validate plug-in path attributes point to existing file paths. """ @@ -22,6 +24,7 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): families = ["workfile"] label = "Plug-in Path Attributes" actions = [SelectInvalidAction] + optional = False # Attributes are defined in project settings attribute = [] @@ -60,6 +63,8 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): def process(self, instance): """Process all directories Set as Filenames in Non-Maya Nodes""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_render_image_rule.py b/client/ayon_core/hosts/maya/plugins/publish/validate_render_image_rule.py index 384d99df1a..117f7df822 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -5,10 +5,15 @@ import pyblish.api from maya import cmds from ayon_core.pipeline.publish import ( - PublishValidationError, RepairAction, ValidateContentsOrder) + PublishValidationError, + RepairAction, + ValidateContentsOrder, + OptionalPyblishPluginMixin +) -class ValidateRenderImageRule(pyblish.api.InstancePlugin): +class ValidateRenderImageRule(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates Maya Workpace "images" file rule matches project settings. This validates against the configured default render image folder: @@ -22,9 +27,11 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): hosts = ["maya"] families = ["renderlayer"] actions = [RepairAction] + optional = False def process(self, instance): - + if not self.is_active(instance.data): + return required_images_rule = os.path.normpath( self.get_default_render_image_folder(instance) ) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_render_no_default_cameras.py b/client/ayon_core/hosts/maya/plugins/publish/validate_render_no_default_cameras.py index 32d0470b7f..41c0fa4807 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_render_no_default_cameras.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_render_no_default_cameras.py @@ -6,10 +6,12 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin): +class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure no default (startup) cameras are to be rendered.""" order = ValidateContentsOrder @@ -17,6 +19,7 @@ class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin): families = ['renderlayer'] label = "No Default Cameras Renderable" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False @staticmethod def get_invalid(instance): @@ -32,6 +35,8 @@ class ValidateRenderNoDefaultCameras(pyblish.api.InstancePlugin): def process(self, instance): """Process all the cameras in the instance""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py b/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py index f31059f594..0171318813 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_render_single_camera.py @@ -7,11 +7,13 @@ import ayon_core.hosts.maya.api.action from ayon_core.hosts.maya.api.lib_rendersettings import RenderSettings from ayon_core.pipeline.publish import ( ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): +class ValidateRenderSingleCamera(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate renderable camera count for layer and token. Pipeline is supporting multiple renderable cameras per layer, but image @@ -24,11 +26,14 @@ class ValidateRenderSingleCamera(pyblish.api.InstancePlugin): families = ["renderlayer", "vrayscene"] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False R_CAMERA_TOKEN = re.compile(r'%c|', re.IGNORECASE) def process(self, instance): """Process all the cameras in the instance""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Invalid cameras for render.") diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_renderlayer_aovs.py b/client/ayon_core/hosts/maya/plugins/publish/validate_renderlayer_aovs.py index 90f256ad45..910e336fc1 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_renderlayer_aovs.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_renderlayer_aovs.py @@ -2,10 +2,13 @@ import ayon_api import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import PublishValidationError +from ayon_core.pipeline.publish import ( + PublishValidationError, + OptionalPyblishPluginMixin +) - -class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): +class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate created AOVs / RenderElement is registered in the database Each render element is registered as a product which is formatted based on @@ -26,8 +29,12 @@ class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): hosts = ["maya"] families = ["renderlayer"] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py index be495a8fb9..f05e2b7311 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_contents.py @@ -3,11 +3,13 @@ from maya import cmds import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( PublishValidationError, - ValidateContentsOrder + ValidateContentsOrder, + OptionalPyblishPluginMixin ) -class ValidateRigContents(pyblish.api.InstancePlugin): +class ValidateRigContents(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Ensure rig contains pipeline-critical content Every rig must contain at least two object sets: @@ -21,11 +23,14 @@ class ValidateRigContents(pyblish.api.InstancePlugin): hosts = ["maya"] families = ["rig"] action = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = True accepted_output = ["mesh", "transform"] accepted_controllers = ["transform"] def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( @@ -87,9 +92,9 @@ class ValidateRigContents(pyblish.api.InstancePlugin): """Validate missing objectsets in rig sets Args: - instance (str): instance - required_objsets (list): list of objectset names - rig_sets (list): list of rig sets + instance (pyblish.api.Instance): instance + required_objsets (list[str]): list of objectset names + rig_sets (list[str]): list of rig sets Raises: PublishValidationError: When the error is raised, it will show @@ -109,15 +114,15 @@ class ValidateRigContents(pyblish.api.InstancePlugin): Check if all rig set members are within the hierarchy of the rig root Args: - instance (str): instance - content (list): list of content from rig sets + instance (pyblish.api.Instance): instance + content (list[str]): list of content from rig sets Raises: PublishValidationError: It means no dag nodes in the rig instance Returns: - list: invalid hierarchy + List[str]: invalid hierarchy """ # Ensure there are at least some transforms or dag nodes # in the rig instance @@ -140,15 +145,13 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def validate_geometry(cls, set_members): - """ - Checks if the node types of the set members valid + """Checks if the node types of the set members valid Args: - set_members: list of nodes of the controls_set - hierarchy: list of nodes which reside under the root node + set_members (list[str]): nodes of the out_set Returns: - errors (list) + list[str]: Nodes of invalid types. """ # Validate all shape types @@ -162,18 +165,17 @@ class ValidateRigContents(pyblish.api.InstancePlugin): if cmds.nodeType(shape) not in cls.accepted_output: invalid.append(shape) + return invalid + @classmethod def validate_controls(cls, set_members): - """ - Checks if the control set members are allowed node types. - Checks if the node types of the set members valid + """Checks if the node types of the set members are valid for controls. Args: - set_members: list of nodes of the controls_set - hierarchy: list of nodes which reside under the root node + set_members (list[str]): list of nodes of the controls_set Returns: - errors (list) + list: Controls of disallowed node types. """ # Validate control types @@ -189,7 +191,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): """Get the target objectsets and rig sets nodes Args: - instance (str): instance + instance (pyblish.api.Instance): instance Returns: tuple: 2-tuple of list of objectsets, @@ -213,6 +215,7 @@ class ValidateSkeletonRigContents(ValidateRigContents): label = "Skeleton Rig Contents" hosts = ["maya"] families = ["rig.fbx"] + optional = True @classmethod def get_invalid(cls, instance): @@ -247,11 +250,10 @@ class ValidateSkeletonRigContents(ValidateRigContents): """Get the target objectsets and rig sets nodes Args: - instance (str): instance + instance (pyblish.api.Instance): instance Returns: - tuple: 2-tuple of list of objectsets, - list of rig sets nodes + tuple: 2-tuple of list of objectsets, list of rig sets nodes """ objectsets = ["skeletonMesh_SET"] skeleton_mesh_nodes = instance.data.get("skeleton_mesh", []) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_controllers.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_controllers.py index 469412dd1a..814ff50177 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -5,13 +5,15 @@ import pyblish.api from ayon_core.pipeline.publish import ( ValidateContentsOrder, RepairAction, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) import ayon_core.hosts.maya.api.action from ayon_core.hosts.maya.api.lib import undo_chunk -class ValidateRigControllers(pyblish.api.InstancePlugin): +class ValidateRigControllers(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate rig controllers. Controls must have the transformation attributes on their default @@ -33,6 +35,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): label = "Rig Controllers" hosts = ["maya"] families = ["rig"] + optional = True actions = [RepairAction, ayon_core.hosts.maya.api.action.SelectInvalidAction] @@ -50,6 +53,9 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): } def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py index 2227899a5b..ea2de81036 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py @@ -5,14 +5,16 @@ import pyblish.api from ayon_core.pipeline.publish import ( ValidateContentsOrder, RepairAction, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) from ayon_core.hosts.maya.api import lib import ayon_core.hosts.maya.api.action -class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): +class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate rig control curves have no keyable arnold attributes. The Arnold plug-in will create curve attributes like: @@ -35,6 +37,7 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): label = "Rig Controllers (Arnold Attributes)" hosts = ["maya"] families = ["rig"] + optional = False actions = [RepairAction, ayon_core.hosts.maya.api.action.SelectInvalidAction] @@ -48,6 +51,9 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): ] def process(self, instance): + if not self.is_active(instance.data): + return + invalid = self.get_invalid(instance) if invalid: raise PublishValidationError('{} failed, see log ' diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_joints_hidden.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_joints_hidden.py index bb5ec8353e..78cc3f5938 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_joints_hidden.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_joints_hidden.py @@ -7,11 +7,13 @@ from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateRigJointsHidden(pyblish.api.InstancePlugin): +class ValidateRigJointsHidden(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate all joints are hidden visually. This includes being hidden: @@ -28,6 +30,7 @@ class ValidateRigJointsHidden(pyblish.api.InstancePlugin): label = "Joints Hidden" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction] + optional = True @staticmethod def get_invalid(instance): @@ -36,6 +39,8 @@ class ValidateRigJointsHidden(pyblish.api.InstancePlugin): def process(self, instance): """Process all the nodes in the instance 'objectSet'""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index dccf9cc47b..ab8cc25210 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -7,11 +7,13 @@ from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( RepairAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): +class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate if deformed shapes have related IDs to the original shapes. When a deformer is applied in the scene on a referenced mesh that already @@ -30,10 +32,12 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): RepairAction ] allow_history_only = False + optional = False def process(self, instance): """Process all meshes""" - + if not self.is_active(instance.data): + return # Ensure all nodes have a cbId and a related ID to the original shapes # if a deformer has been created on the shape invalid = self.get_invalid(instance) @@ -114,6 +118,7 @@ class ValidateSkeletonRigOutSetNodeIds(ValidateRigOutSetNodeIds): families = ["rig.fbx"] hosts = ['maya'] label = 'Skeleton Rig Out Set Node Ids' + optional = False @classmethod def get_node(cls, instance): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py index ffdb43ef55..2783a6dbe8 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_render_stats.py @@ -6,10 +6,12 @@ import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, ValidateMeshOrder, + OptionalPyblishPluginMixin ) -class ValidateShapeRenderStats(pyblish.api.Validator): +class ValidateShapeRenderStats(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Ensure all render stats are set to the default values.""" order = ValidateMeshOrder @@ -18,6 +20,7 @@ class ValidateShapeRenderStats(pyblish.api.Validator): label = 'Shape Default Render Stats' actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction] + optional = True defaults = {'castsShadows': 1, 'receiveShadows': 1, @@ -46,7 +49,8 @@ class ValidateShapeRenderStats(pyblish.api.Validator): return invalid def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py index 6cf3edf472..4f4826776c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shape_zero.py @@ -7,11 +7,13 @@ from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( ValidateContentsOrder, RepairAction, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateShapeZero(pyblish.api.Validator): +class ValidateShapeZero(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Shape components may not have any "tweak" values To solve this issue, try freezing the shapes. @@ -26,6 +28,7 @@ class ValidateShapeZero(pyblish.api.Validator): ayon_core.hosts.maya.api.action.SelectInvalidAction, RepairAction ] + optional = True @staticmethod def get_invalid(instance): @@ -65,6 +68,8 @@ class ValidateShapeZero(pyblish.api.Validator): def process(self, instance): """Process all the nodes in the instance "objectSet""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py b/client/ayon_core/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py index ff2ad822b6..172453f1ef 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_skeletalmesh_hierarchy.py @@ -4,20 +4,25 @@ import pyblish.api from ayon_core.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, + OptionalPyblishPluginMixin ) from maya import cmds -class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin): +class ValidateSkeletalMeshHierarchy(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates that nodes has common root.""" order = ValidateContentsOrder hosts = ["maya"] families = ["skeletalMesh"] label = "Skeletal Mesh Top Node" + optional = False def process(self, instance): + if not self.is_active(instance.data): + return geo = instance.data.get("geometry") joints = instance.data.get("joints") diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py b/client/ayon_core/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py index 7c876240ae..9fbe0f440b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py @@ -21,8 +21,12 @@ class ValidateSkeletonTopGroupHierarchy(pyblish.api.InstancePlugin, order = ValidateContentsOrder + 0.05 label = "Skeleton Rig Top Group Hierarchy" families = ["rig.fbx"] + optional = True def process(self, instance): + if not self.is_active(instance.data): + return + invalid = [] skeleton_mesh_data = instance.data("skeleton_mesh", []) if skeleton_mesh_data: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py b/client/ayon_core/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py index c104f0477f..48d8e63553 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_skinCluster_deformer_set.py @@ -3,10 +3,11 @@ from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline.publish import ValidateContentsOrder,OptionalPyblishPluginMixin -class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin): +class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate skinClusters on meshes have valid member relationships. In rare cases it can happen that a mesh has a skinCluster in its history @@ -20,9 +21,12 @@ class ValidateSkinclusterDeformerSet(pyblish.api.InstancePlugin): families = ['fbx'] label = "Skincluster Deformer Relationships" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False def process(self, instance): """Process all the transform nodes in the instance""" + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py b/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py index 524c7b29ba..a3419a83a9 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_step_size.py @@ -3,11 +3,13 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( PublishValidationError, - ValidateContentsOrder + ValidateContentsOrder, + OptionalPyblishPluginMixin ) -class ValidateStepSize(pyblish.api.InstancePlugin): +class ValidateStepSize(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates the step size for the instance is in a valid range. For example the `step` size should never be lower or equal to zero. @@ -20,7 +22,7 @@ class ValidateStepSize(pyblish.api.InstancePlugin): 'pointcache', 'animation'] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] - + optional = False MIN = 0.01 MAX = 1.0 @@ -40,7 +42,8 @@ class ValidateStepSize(pyblish.api.InstancePlugin): return [] def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py index ddf9d3867d..1cbdd05b0b 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_transform_zero.py @@ -5,11 +5,13 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateTransformZero(pyblish.api.Validator): +class ValidateTransformZero(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Transforms can't have any values To solve this issue, try freezing the transforms. So long @@ -29,6 +31,7 @@ class ValidateTransformZero(pyblish.api.Validator): 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] _tolerance = 1e-30 + optional = True @classmethod def get_invalid(cls, instance): @@ -62,7 +65,8 @@ class ValidateTransformZero(pyblish.api.Validator): def process(self, instance): """Process all the nodes in the instance "objectSet""" - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py b/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py index 55f8933fff..8ec704ddd1 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_unique_names.py @@ -2,10 +2,13 @@ from maya import cmds import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin +) - -class ValidateUniqueNames(pyblish.api.Validator): +class ValidateUniqueNames(pyblish.api.Validator, + OptionalPyblishPluginMixin): """transform names should be unique ie: using cmds.ls(someNodeName) should always return shortname @@ -17,6 +20,7 @@ class ValidateUniqueNames(pyblish.api.Validator): families = ["model"] label = "Unique transform name" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = True @staticmethod def get_invalid(instance): @@ -32,7 +36,8 @@ class ValidateUniqueNames(pyblish.api.Validator): def process(self, instance): """Process all the nodes in the instance "objectSet""" - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise ValueError("Nodes found with none unique names. " diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py b/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py index a39ba7c4cc..101bd5bf04 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_mesh_triangulated.py @@ -3,11 +3,15 @@ from maya import cmds import pyblish.api -from ayon_core.pipeline.publish import ValidateMeshOrder +from ayon_core.pipeline.publish import ( + ValidateMeshOrder, + OptionalPyblishPluginMixin +) import ayon_core.hosts.maya.api.action -class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): +class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate if mesh is made of triangles for Unreal Engine""" order = ValidateMeshOrder @@ -30,6 +34,8 @@ class ValidateUnrealMeshTriangulated(pyblish.api.InstancePlugin): return invalid def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) assert len(invalid) == 0, ( "Found meshes without triangles") diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_visible_only.py b/client/ayon_core/hosts/maya/plugins/publish/validate_visible_only.py index 29cf9420a3..af6c9a64c6 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_visible_only.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_visible_only.py @@ -4,11 +4,13 @@ from ayon_core.hosts.maya.api.lib import iter_visible_nodes_in_range import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin): +class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validates at least a single node is visible in frame range. This validation only validates if the `visibleOnly` flag is enabled @@ -20,9 +22,11 @@ class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin): hosts = ["maya"] families = ["pointcache", "animation"] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False def process(self, instance): - + if not self.is_active(instance.data): + return if not instance.data.get("visibleOnly", False): self.log.debug("Visible only is disabled. Validation skipped..") return diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py index 54eaa58e74..b35508d635 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_distributed_rendering.py @@ -3,10 +3,15 @@ from maya import cmds from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( - PublishValidationError, RepairAction, ValidateContentsOrder) + PublishValidationError, + RepairAction, + ValidateContentsOrder, + OptionalPyblishPluginMixin +) -class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin): +class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate V-Ray Distributed Rendering is ignored in batch mode. Whenever Distributed Rendering is enabled for V-Ray in the render settings @@ -20,13 +25,15 @@ class ValidateVRayDistributedRendering(pyblish.api.InstancePlugin): label = "VRay Distributed Rendering" families = ["renderlayer"] actions = [RepairAction] + optional = False # V-Ray attribute names enabled_attr = "vraySettings.sys_distributed_rendering_on" ignored_attr = "vraySettings.sys_distributed_rendering_ignore_batch" def process(self, instance): - + if not self.is_active(instance.data): + return if instance.data.get("renderer") != "vray": # If not V-Ray ignore.. return diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py index d4e53d69dc..7c480a6bf7 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py @@ -4,10 +4,13 @@ import pyblish.api import types from maya import cmds -from ayon_core.pipeline.publish import RepairContextAction +from ayon_core.pipeline.publish import ( + RepairContextAction, + OptionalPyblishPluginMixin +) - -class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): +class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate whether the V-Ray Render Elements (AOVs) include references. This will check if there are AOVs pulled from references. If @@ -21,9 +24,12 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): hosts = ['maya'] families = ['renderlayer'] actions = [RepairContextAction] + optional = False def process(self, instance): """Plugin main entry point.""" + if not self.is_active(instance.data): + return if instance.data.get("renderer") != "vray": # If not V-Ray ignore.. return @@ -39,7 +45,7 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin): self.log.warning(( "Referenced AOVs are enabled in Vray " "Render Settings and are detected in scene, but " - "Pype render instance option for referenced AOVs is " + "AYON render instance option for referenced AOVs is " "disabled. Those AOVs will be rendered but not published " "by Pype." )) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_translator_settings.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_translator_settings.py index f366ee60cf..a3d93dd9c0 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vray_translator_settings.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vray_translator_settings.py @@ -5,22 +5,27 @@ from ayon_core.pipeline.publish import ( context_plugin_should_run, RepairContextAction, ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) from maya import cmds -class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin): +class ValidateVRayTranslatorEnabled(pyblish.api.ContextPlugin, + OptionalPyblishPluginMixin): """Validate VRay Translator settings for extracting vrscenes.""" order = ValidateContentsOrder label = "VRay Translator Settings" families = ["vrayscene_layer"] actions = [RepairContextAction] + optional = False def process(self, context): """Plugin entry point.""" + if not self.is_active(context.data): + return # Workaround bug pyblish-base#250 if not context_plugin_should_run(self, context): return diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py index 7e16006f97..29b8be411c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy.py @@ -1,18 +1,21 @@ import pyblish.api from ayon_core.pipeline import KnownPublishError +from ayon_core.pipeline.publish import OptionalPyblishPluginMixin - -class ValidateVrayProxy(pyblish.api.InstancePlugin): +class ValidateVrayProxy(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): order = pyblish.api.ValidatorOrder label = "VRay Proxy Settings" hosts = ["maya"] families = ["vrayproxy"] + optional = False def process(self, instance): data = instance.data - + if not self.is_active(data): + return if not data["setMembers"]: raise KnownPublishError( "'%s' is empty! This is a bug" % instance.name diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy_members.py b/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy_members.py index 1a52771ee6..6732d09202 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy_members.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_vrayproxy_members.py @@ -4,12 +4,14 @@ from maya import cmds import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateVrayProxyMembers(pyblish.api.InstancePlugin): +class ValidateVrayProxyMembers(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate whether the V-Ray Proxy instance has shape members""" order = pyblish.api.ValidatorOrder @@ -17,9 +19,11 @@ class ValidateVrayProxyMembers(pyblish.api.InstancePlugin): hosts = ['maya'] families = ['vrayproxy'] actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py index a72d930339..35b2443718 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_renderscript_callbacks.py @@ -1,10 +1,13 @@ from maya import cmds import pyblish.api -from ayon_core.pipeline.publish import ValidateContentsOrder +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin +) - -class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin): +class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Check if the render script callbacks will be used during the rendering In order to ensure the render tasks are executed properly we need to check @@ -24,6 +27,7 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin): label = "Yeti Render Script Callbacks" hosts = ["maya"] families = ["renderlayer"] + optional = False # Settings per renderer callbacks = { @@ -37,7 +41,8 @@ class ValidateYetiRenderScriptCallbacks(pyblish.api.InstancePlugin): } def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise ValueError("Invalid render callbacks found for '%s'!" diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py index 22545d07fb..d81534192a 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_cache_state.py @@ -3,12 +3,14 @@ import maya.cmds as cmds import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( RepairAction, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateYetiRigCacheState(pyblish.api.InstancePlugin): +class ValidateYetiRigCacheState(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate the I/O attributes of the node Every pgYetiMaya cache node per instance should have: @@ -23,8 +25,11 @@ class ValidateYetiRigCacheState(pyblish.api.InstancePlugin): families = ["yetiRig"] actions = [RepairAction, ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False def process(self, instance): + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Nodes have incorrect I/O settings") diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py index 3d9d8faca8..aa229875fe 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_input_in_instance.py @@ -5,11 +5,13 @@ import pyblish.api import ayon_core.hosts.maya.api.action from ayon_core.pipeline.publish import ( ValidateContentsOrder, - PublishValidationError + PublishValidationError, + OptionalPyblishPluginMixin ) -class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator): +class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator, + OptionalPyblishPluginMixin): """Validate if all input nodes are part of the instance's hierarchy""" order = ValidateContentsOrder @@ -17,9 +19,11 @@ class ValidateYetiRigInputShapesInInstance(pyblish.api.Validator): families = ["yetiRig"] label = "Yeti Rig Input Shapes In Instance" actions = [ayon_core.hosts.maya.api.action.SelectInvalidAction] + optional = False def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError("Yeti Rig has invalid input meshes") diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_settings.py b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_settings.py index 22a5ccbaca..6bd2ebb753 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_settings.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_yeti_rig_settings.py @@ -1,9 +1,13 @@ import pyblish.api -from ayon_core.pipeline.publish import PublishValidationError +from ayon_core.pipeline.publish import ( + PublishValidationError, + OptionalPyblishPluginMixin +) -class ValidateYetiRigSettings(pyblish.api.InstancePlugin): +class ValidateYetiRigSettings(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Validate Yeti Rig Settings have collected input connections. The input connections are collected for the nodes in the `input_SET`. @@ -15,9 +19,11 @@ class ValidateYetiRigSettings(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Yeti Rig Settings" families = ["yetiRig"] + optional = False def process(self, instance): - + if not self.is_active(instance.data): + return invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( diff --git a/client/ayon_core/hosts/maya/startup/userSetup.py b/client/ayon_core/hosts/maya/startup/userSetup.py index adbbfe4f44..3112e2bf12 100644 --- a/client/ayon_core/hosts/maya/startup/userSetup.py +++ b/client/ayon_core/hosts/maya/startup/userSetup.py @@ -10,7 +10,7 @@ from maya import cmds host = MayaHost() install_host(host) -print("Starting OpenPype usersetup...") +print("Starting AYON usersetup...") project_name = get_current_project_name() settings = get_project_settings(project_name) @@ -47,4 +47,4 @@ if bool(int(os.environ.get(key, "0"))): ) -print("Finished OpenPype usersetup.") +print("Finished AYON usersetup.") diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index 8e39475f10..1bb0ff79e0 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -982,26 +982,18 @@ def format_anatomy(data): project_name = get_current_project_name() anatomy = Anatomy(project_name) - log.debug("__ anatomy.templates: {}".format(anatomy.templates)) - padding = None - if "frame_padding" in anatomy.templates.keys(): - padding = int(anatomy.templates["frame_padding"]) - elif "render" in anatomy.templates.keys(): - padding = int( - anatomy.templates["render"].get( - "frame_padding" - ) - ) + frame_padding = anatomy.templates_obj.frame_padding - version = data.get("version", None) - if not version: + version = data.get("version") + if version is None: file = script_name() data["version"] = get_version_from_path(file) folder_path = data["folderPath"] task_name = data["task"] host_name = get_current_host_name() + context_data = get_template_data_with_names( project_name, folder_path, task_name, host_name ) @@ -1013,7 +1005,7 @@ def format_anatomy(data): "name": data["productName"], "type": data["productType"], }, - "frame": "#" * padding, + "frame": "#" * frame_padding, }) return anatomy.format(data) @@ -1171,7 +1163,9 @@ def create_write_node( anatomy_filled = format_anatomy(data) # build file path to workfiles - fdir = str(anatomy_filled["work"]["folder"]).replace("\\", "/") + fdir = str( + anatomy_filled["work"]["default"]["directory"] + ).replace("\\", "/") data["work"] = fdir fpath = StringTemplate(data["fpath_template"]).format_strict(data) diff --git a/client/ayon_core/hosts/nuke/api/pipeline.py b/client/ayon_core/hosts/nuke/api/pipeline.py index a8876f6aa7..2255276c56 100644 --- a/client/ayon_core/hosts/nuke/api/pipeline.py +++ b/client/ayon_core/hosts/nuke/api/pipeline.py @@ -128,7 +128,7 @@ class NukeHost( register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) - # Register Avalon event for workfiles loading. + # Register AYON event for workfiles loading. register_event_callback("workio.open_file", check_inventory_versions) register_event_callback("taskChanged", change_context_label) @@ -230,9 +230,9 @@ def get_context_label(): def _install_menu(): - """Install Avalon menu into Nuke's main menu bar.""" + """Install AYON menu into Nuke's main menu bar.""" - # uninstall original avalon menu + # uninstall original AYON menu main_window = get_main_window() menubar = nuke.menu("Nuke") menu = menubar.addMenu(MENU_LABEL) diff --git a/client/ayon_core/hosts/nuke/startup/custom_write_node.py b/client/ayon_core/hosts/nuke/startup/custom_write_node.py index 075c8e7a17..f119e69919 100644 --- a/client/ayon_core/hosts/nuke/startup/custom_write_node.py +++ b/client/ayon_core/hosts/nuke/startup/custom_write_node.py @@ -1,8 +1,8 @@ -""" OpenPype custom script for setting up write nodes for non-publish """ +""" AYON custom script for setting up write nodes for non-publish """ import os import nuke import nukescripts -from ayon_core.pipeline import Anatomy +from ayon_core.pipeline import Anatomy, get_current_project_name from ayon_core.hosts.nuke.api.lib import ( set_node_knobs_from_settings, get_nuke_imageio_settings @@ -102,13 +102,9 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): for knob in ext_knob_list: ext = knob["value"] - anatomy = Anatomy() + anatomy = Anatomy(get_current_project_name()) - frame_padding = int( - anatomy.templates["render"].get( - "frame_padding" - ) - ) + frame_padding = anatomy.templates_obj.frame_padding for write_node in write_selected_nodes: # data for mapping the path # TODO add more fill data diff --git a/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py b/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py index f0cbabe20f..3e1430c3b1 100644 --- a/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py +++ b/client/ayon_core/hosts/nuke/startup/frame_setting_for_read_nodes.py @@ -1,4 +1,4 @@ -""" OpenPype custom script for resetting read nodes start frame values """ +""" AYON custom script for resetting read nodes start frame values """ import nuke import nukescripts diff --git a/client/ayon_core/hosts/photoshop/api/launch_logic.py b/client/ayon_core/hosts/photoshop/api/launch_logic.py index 17fe7d5920..d0823646d7 100644 --- a/client/ayon_core/hosts/photoshop/api/launch_logic.py +++ b/client/ayon_core/hosts/photoshop/api/launch_logic.py @@ -392,15 +392,13 @@ class PhotoshopRoute(WebSocketRoute): ) data["root"] = anatomy.roots - file_template = anatomy.templates[template_key]["file"] + work_template = anatomy.get_template_item("work", template_key) # Define saving file extension extensions = host.get_workfile_extensions() - folder_template = anatomy.templates[template_key]["folder"] - work_root = StringTemplate.format_strict_template( - folder_template, data - ) + work_root = work_template["directory"].format_strict(data) + file_template = work_template["file"].template last_workfile_path = get_last_workfile( work_root, file_template, data, extensions, True ) diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py index b488ab364d..adbe02eb74 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -9,7 +9,6 @@ class CollectAutoImage(pyblish.api.ContextPlugin): """ label = "Collect Auto Image" - order = pyblish.api.CollectorOrder hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.2 diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py index 0585f4f226..7a5f297c89 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image_refresh.py @@ -8,7 +8,6 @@ class CollectAutoImageRefresh(pyblish.api.ContextPlugin): """ label = "Collect Auto Image Refresh" - order = pyblish.api.CollectorOrder hosts = ["photoshop"] order = pyblish.api.CollectorOrder + 0.2 diff --git a/client/ayon_core/hosts/resolve/README.markdown b/client/ayon_core/hosts/resolve/README.markdown index a8bb071e7e..064e791f65 100644 --- a/client/ayon_core/hosts/resolve/README.markdown +++ b/client/ayon_core/hosts/resolve/README.markdown @@ -18,7 +18,7 @@ This is how it looks on my testing project timeline ![image](https://user-images.githubusercontent.com/40640033/102637638-96ec6600-4156-11eb-9656-6e8e3ce4baf8.png) Notice I had renamed tracks to `main` (holding metadata markers) and `review` used for generating review data with ffmpeg confersion to jpg sequence. -1. you need to start OpenPype menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__OpenPype_Menu__** +1. you need to start AYON menu from Resolve/EditTab/Menu/Workspace/Scripts/Comp/**__OpenPype_Menu__** 2. then select any clips in `main` track and change their color to `Chocolate` 3. in OpenPype Menu select `Create` 4. in Creator select `Create Publishable Clip [New]` (temporary name) diff --git a/client/ayon_core/hosts/resolve/api/__init__.py b/client/ayon_core/hosts/resolve/api/__init__.py index dba275e6c4..3359430ef5 100644 --- a/client/ayon_core/hosts/resolve/api/__init__.py +++ b/client/ayon_core/hosts/resolve/api/__init__.py @@ -44,7 +44,7 @@ from .lib import ( get_reformated_path ) -from .menu import launch_pype_menu +from .menu import launch_ayon_menu from .plugin import ( ClipLoader, @@ -113,7 +113,7 @@ __all__ = [ "get_reformated_path", # menu - "launch_pype_menu", + "launch_ayon_menu", # plugin "ClipLoader", diff --git a/client/ayon_core/hosts/resolve/api/menu.py b/client/ayon_core/hosts/resolve/api/menu.py index 59eba14d83..dd8573acc0 100644 --- a/client/ayon_core/hosts/resolve/api/menu.py +++ b/client/ayon_core/hosts/resolve/api/menu.py @@ -38,9 +38,9 @@ class Spacer(QtWidgets.QWidget): self.setLayout(layout) -class OpenPypeMenu(QtWidgets.QWidget): +class AYONMenu(QtWidgets.QWidget): def __init__(self, *args, **kwargs): - super(OpenPypeMenu, self).__init__(*args, **kwargs) + super(AYONMenu, self).__init__(*args, **kwargs) self.setObjectName(f"{MENU_LABEL}Menu") @@ -170,14 +170,14 @@ class OpenPypeMenu(QtWidgets.QWidget): host_tools.show_experimental_tools_dialog() -def launch_pype_menu(): +def launch_ayon_menu(): app = QtWidgets.QApplication(sys.argv) - pype_menu = OpenPypeMenu() + ayon_menu = AYONMenu() stylesheet = load_stylesheet() - pype_menu.setStyleSheet(stylesheet) + ayon_menu.setStyleSheet(stylesheet) - pype_menu.show() + ayon_menu.show() sys.exit(app.exec_()) diff --git a/client/ayon_core/hosts/resolve/api/menu_style.qss b/client/ayon_core/hosts/resolve/api/menu_style.qss index 3d51c7139f..ad8932d881 100644 --- a/client/ayon_core/hosts/resolve/api/menu_style.qss +++ b/client/ayon_core/hosts/resolve/api/menu_style.qss @@ -51,7 +51,7 @@ QLineEdit { qproperty-alignment: AlignCenter; } -#OpenPypeMenu { +#AYONMenu { qproperty-alignment: AlignLeft; min-width: 10em; border: 1px solid #fef9ef; diff --git a/client/ayon_core/hosts/resolve/startup.py b/client/ayon_core/hosts/resolve/startup.py index b3c1a024d9..3ad0a6bf7b 100644 --- a/client/ayon_core/hosts/resolve/startup.py +++ b/client/ayon_core/hosts/resolve/startup.py @@ -35,7 +35,7 @@ def ensure_installed_host(): def launch_menu(): print("Launching Resolve AYON menu..") ensure_installed_host() - ayon_core.hosts.resolve.api.launch_pype_menu() + ayon_core.hosts.resolve.api.launch_ayon_menu() def open_workfile(path): diff --git a/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py b/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py index 08cefb9d61..b10b477beb 100644 --- a/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py +++ b/client/ayon_core/hosts/resolve/utility_scripts/AYON__Menu.py @@ -8,13 +8,13 @@ log = Logger.get_logger(__name__) def main(env): - from ayon_core.hosts.resolve.api import ResolveHost, launch_pype_menu + from ayon_core.hosts.resolve.api import ResolveHost, launch_ayon_menu # activate resolve from openpype host = ResolveHost() install_host(host) - launch_pype_menu() + launch_ayon_menu() if __name__ == "__main__": diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py index a3bcf4c144..ec671b0cb1 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py @@ -1,10 +1,7 @@ import collections from ayon_core.lib.attribute_definitions import BoolDef -from ayon_core.pipeline import ( - get_representation_context, - registered_host, -) +from ayon_core.pipeline import registered_host from ayon_core.hosts.tvpaint.api import plugin from ayon_core.hosts.tvpaint.api.lib import ( get_layers_data, @@ -218,10 +215,7 @@ class LoadImage(plugin.Loader): removed. """ - repre_entity = context["representation"] # Create new containers first - context = get_representation_context(repre_entity["id"]) - # Get layer ids from previous container old_layer_names = self.get_members_from_container(container) diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py index 182d95d6db..d8c6a7a430 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_sound.py @@ -54,7 +54,7 @@ class ImportSound(plugin.Loader): def load(self, context, name, namespace, options): # Create temp file for output output_file = tempfile.NamedTemporaryFile( - mode="w", prefix="pype_tvp_", suffix=".txt", delete=False + mode="w", prefix="ayon_tvp_", suffix=".txt", delete=False ) output_file.close() output_filepath = output_file.name.replace("\\", "/") diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py index 56aa67708d..8663dac997 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py @@ -80,7 +80,7 @@ class LoadWorkfile(plugin.Loader): ) data["root"] = anatomy.roots - file_template = anatomy.templates[template_key]["file"] + work_template = anatomy.get_template_item("work", template_key) # Define saving file extension extensions = host.get_workfile_extensions() @@ -91,14 +91,11 @@ class LoadWorkfile(plugin.Loader): # Fall back to the first extension supported for this host. extension = extensions[0] - data["ext"] = extension + data["ext"] = extension.lstrip(".") - folder_template = anatomy.templates[template_key]["folder"] - work_root = StringTemplate.format_strict_template( - folder_template, data - ) + work_root = work_template["directory"].format_strict(data) version = get_last_workfile_with_version( - work_root, file_template, data, extensions + work_root, work_template["file"].template, data, extensions )[1] if version is None: diff --git a/client/ayon_core/hosts/unreal/api/pipeline.py b/client/ayon_core/hosts/unreal/api/pipeline.py index 1f937437a4..a60564d5b0 100644 --- a/client/ayon_core/hosts/unreal/api/pipeline.py +++ b/client/ayon_core/hosts/unreal/api/pipeline.py @@ -98,7 +98,7 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): def install(): - """Install Unreal configuration for OpenPype.""" + """Install Unreal configuration for AYON.""" print("-=" * 40) logo = '''. . diff --git a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py index 2f78bf026d..54ffba3a63 100644 --- a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py @@ -66,7 +66,9 @@ class UnrealPrelaunchHook(PreLaunchHook): self.host_name, ) # Fill templates - template_obj = anatomy.templates_obj[workfile_template_key]["file"] + template_obj = anatomy.get_template_item( + "work", workfile_template_key, "file" + ) # Return filename return template_obj.format_strict(workdir_data) diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index f69fd10b07..d23d807b32 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -81,11 +81,8 @@ from .log import ( ) from .path_templates import ( - merge_dict, - TemplateMissingKey, TemplateUnsolved, StringTemplate, - TemplatesDict, FormatObject, ) @@ -259,11 +256,8 @@ __all__ = [ "get_version_from_path", "get_last_version_from_path", - "merge_dict", - "TemplateMissingKey", "TemplateUnsolved", "StringTemplate", - "TemplatesDict", "FormatObject", "terminal", diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 4bf0c31d93..444a4cf67e 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -204,7 +204,7 @@ class ApplicationGroup: Application group wraps different versions(variants) of application. e.g. "maya" is group and "maya_2020" is variant. - Group hold `host_name` which is implementation name used in pype. Also + Group hold `host_name` which is implementation name used in AYON. Also holds `enabled` if whole app group is enabled or `icon` for application icon path in resources. @@ -1862,7 +1862,9 @@ def _prepare_last_workfile(data, workdir, addons_manager): project_settings=project_settings ) # Find last workfile - file_template = str(anatomy.templates[template_key]["file"]) + file_template = anatomy.get_template_item( + "work", template_key, "file" + ).template workdir_data.update({ "version": 1, @@ -1897,12 +1899,12 @@ def should_start_last_workfile( `"0", "1", "true", "false", "yes", "no"`. Args: - project_name (str): Name of project. - host_name (str): Name of host which is launched. In avalon's - application context it's value stored in app definition under - key `"application_dir"`. Is not case sensitive. - task_name (str): Name of task which is used for launching the host. - Task name is not case sensitive. + project_name (str): Project name. + host_name (str): Host name. + task_name (str): Task name. + task_type (str): Task type. + default_output (Optional[bool]): Default output if no profile is + found. Returns: bool: True if host should start workfile. @@ -1947,12 +1949,12 @@ def should_workfile_tool_start( `"0", "1", "true", "false", "yes", "no"`. Args: - project_name (str): Name of project. - host_name (str): Name of host which is launched. In avalon's - application context it's value stored in app definition under - key `"application_dir"`. Is not case sensitive. - task_name (str): Name of task which is used for launching the host. - Task name is not case sensitive. + project_name (str): Project name. + host_name (str): Host name. + task_name (str): Task name. + task_type (str): Task type. + default_output (Optional[bool]): Default output if no profile is + found. Returns: bool: True if host should start workfile. diff --git a/client/ayon_core/lib/ayon_connection.py b/client/ayon_core/lib/ayon_connection.py index 5ee654943f..1132d77aaa 100644 --- a/client/ayon_core/lib/ayon_connection.py +++ b/client/ayon_core/lib/ayon_connection.py @@ -10,6 +10,87 @@ class _Cache: initialized = False +def _new_get_last_versions( + self, + project_name, + product_ids, + active=True, + fields=None, + own_attributes=False +): + """Query last version entities by product ids. + + Args: + project_name (str): Project where to look for representation. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + dict[str, dict[str, Any]]: Last versions by product id. + + """ + if fields: + fields = set(fields) + fields.add("productId") + + versions = self.get_versions( + project_name, + product_ids=product_ids, + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + return { + version["productId"]: version + for version in versions + } + + +def _new_get_last_version_by_product_id( + self, + project_name, + product_id, + active=True, + fields=None, + own_attributes=False +): + """Query last version entity by product id. + + Args: + project_name (str): Project where to look for representation. + product_id (str): Product id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Queried version entity or None. + + """ + versions = self.get_versions( + project_name, + product_ids=[product_id], + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def _new_get_last_version_by_product_name( self, project_name, @@ -73,9 +154,15 @@ def initialize_ayon_connection(force=False): semver.VersionInfo.parse(ayon_api.__version__).to_tuple() ) # TODO remove mokey patching after when AYON api is safely updated - fix_last_version_by_product_name = ayon_api_version < (1, 0, 2) + fix_before_1_0_2 = ayon_api_version < (1, 0, 2) # Monkey patching to fix 'get_last_version_by_product_name' - if fix_last_version_by_product_name: + if fix_before_1_0_2: + ayon_api.ServerAPI.get_last_versions = ( + _new_get_last_versions + ) + ayon_api.ServerAPI.get_last_version_by_product_id = ( + _new_get_last_version_by_product_id + ) ayon_api.ServerAPI.get_last_version_by_product_name = ( _new_get_last_version_by_product_name ) @@ -85,12 +172,22 @@ def initialize_ayon_connection(force=False): if ayon_api.is_connection_created(): con = ayon_api.get_server_api_connection() # Monkey patching to fix 'get_last_version_by_product_name' - if fix_last_version_by_product_name: - def _con_wrapper(*args, **kwargs): + if fix_before_1_0_2: + def _lvs_wrapper(*args, **kwargs): + return _new_get_last_versions( + con, *args, **kwargs + ) + def _lv_by_pi_wrapper(*args, **kwargs): + return _new_get_last_version_by_product_id( + con, *args, **kwargs + ) + def _lv_by_pn_wrapper(*args, **kwargs): return _new_get_last_version_by_product_name( con, *args, **kwargs ) - con.get_last_version_by_product_name = _con_wrapper + con.get_last_versions = _lvs_wrapper + con.get_last_version_by_product_id = _lv_by_pi_wrapper + con.get_last_version_by_product_name = _lv_by_pn_wrapper con.set_site_id(site_id) con.set_client_version(version) else: diff --git a/client/ayon_core/lib/ayon_info.py b/client/ayon_core/lib/ayon_info.py index ec37d735d8..3975b35bc3 100644 --- a/client/ayon_core/lib/ayon_info.py +++ b/client/ayon_core/lib/ayon_info.py @@ -102,8 +102,8 @@ def get_all_current_info(): def extract_ayon_info_to_file(dirpath, filename=None): """Extract all current info to a file. - It is possible to define only directory path. Filename is concatenated with - pype version, workstation site id and timestamp. + It is possible to define only directory path. Filename is concatenated + with AYON version, workstation site id and timestamp. Args: dirpath (str): Path to directory where file will be stored. diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 9be1736abf..09d11ea1de 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -1,8 +1,6 @@ import os import re -import copy import numbers -import collections import six @@ -12,44 +10,6 @@ SUB_DICT_PATTERN = re.compile(r"([^\[\]]+)") OPTIONAL_PATTERN = re.compile(r"(<.*?[^{0]*>)[^0-9]*?") -def merge_dict(main_dict, enhance_dict): - """Merges dictionaries by keys. - - Function call itself if value on key is again dictionary. - - Args: - main_dict (dict): First dict to merge second one into. - enhance_dict (dict): Second dict to be merged. - - Returns: - dict: Merged result. - - .. note:: does not overrides whole value on first found key - but only values differences from enhance_dict - - """ - for key, value in enhance_dict.items(): - if key not in main_dict: - main_dict[key] = value - elif isinstance(value, dict) and isinstance(main_dict[key], dict): - main_dict[key] = merge_dict(main_dict[key], value) - else: - main_dict[key] = value - return main_dict - - -class TemplateMissingKey(Exception): - """Exception for cases when key does not exist in template.""" - - msg = "Template key does not exist: `{}`." - - def __init__(self, parents): - parent_join = "".join(["[\"{0}\"]".format(key) for key in parents]) - super(TemplateMissingKey, self).__init__( - self.msg.format(parent_join) - ) - - class TemplateUnsolved(Exception): """Exception for unsolved template when strict is set to True.""" @@ -240,137 +200,6 @@ class StringTemplate(object): new_parts.extend(tmp_parts[idx]) return new_parts - -class TemplatesDict(object): - def __init__(self, templates=None): - self._raw_templates = None - self._templates = None - self._objected_templates = None - self.set_templates(templates) - - def set_templates(self, templates): - if templates is None: - self._raw_templates = None - self._templates = None - self._objected_templates = None - elif isinstance(templates, dict): - self._raw_templates = copy.deepcopy(templates) - self._templates = templates - self._objected_templates = self.create_objected_templates( - templates) - else: - raise TypeError("<{}> argument must be a dict, not {}.".format( - self.__class__.__name__, str(type(templates)) - )) - - def __getitem__(self, key): - return self.objected_templates[key] - - def get(self, key, *args, **kwargs): - return self.objected_templates.get(key, *args, **kwargs) - - @property - def raw_templates(self): - return self._raw_templates - - @property - def templates(self): - return self._templates - - @property - def objected_templates(self): - return self._objected_templates - - def _create_template_object(self, template): - """Create template object from a template string. - - Separated into method to give option change class of templates. - - Args: - template (str): Template string. - - Returns: - StringTemplate: Object of template. - """ - - return StringTemplate(template) - - def create_objected_templates(self, templates): - if not isinstance(templates, dict): - raise TypeError("Expected dict object, got {}".format( - str(type(templates)) - )) - - objected_templates = copy.deepcopy(templates) - inner_queue = collections.deque() - inner_queue.append(objected_templates) - while inner_queue: - item = inner_queue.popleft() - if not isinstance(item, dict): - continue - for key in tuple(item.keys()): - value = item[key] - if isinstance(value, six.string_types): - item[key] = self._create_template_object(value) - elif isinstance(value, dict): - inner_queue.append(value) - return objected_templates - - def _format_value(self, value, data): - if isinstance(value, StringTemplate): - return value.format(data) - - if isinstance(value, dict): - return self._solve_dict(value, data) - return value - - def _solve_dict(self, templates, data): - """ Solves templates with entered data. - - Args: - templates (dict): All templates which will be formatted. - data (dict): Containing keys to be filled into template. - - Returns: - dict: With `TemplateResult` in values containing filled or - partially filled templates. - """ - output = collections.defaultdict(dict) - for key, value in templates.items(): - output[key] = self._format_value(value, data) - - return output - - def format(self, in_data, only_keys=True, strict=True): - """ Solves templates based on entered data. - - Args: - data (dict): Containing keys to be filled into template. - only_keys (bool, optional): Decides if environ will be used to - fill templates or only keys in data. - - Returns: - TemplatesResultDict: Output `TemplateResult` have `strict` - attribute set to True so accessing unfilled keys in templates - will raise exceptions with explaned error. - """ - # Create a copy of inserted data - data = copy.deepcopy(in_data) - - # Add environment variable to data - if only_keys is False: - for key, val in os.environ.items(): - env_key = "$" + key - if env_key not in data: - data[env_key] = val - - solved = self._solve_dict(self.objected_templates, data) - - output = TemplatesResultDict(solved) - output.strict = strict - return output - - class TemplateResult(str): """Result of template format with most of information in. @@ -379,8 +208,8 @@ class TemplateResult(str): only used keys. solved (bool): For check if all required keys were filled. template (str): Original template. - missing_keys (list): Missing keys that were not in the data. Include - missing optional keys. + missing_keys (Iterable[str]): Missing keys that were not in the data. + Include missing optional keys. invalid_types (dict): When key was found in data, but value had not allowed DataType. Allowed data types are `numbers`, `str`(`basestring`) and `dict`. Dictionary may cause invalid type @@ -445,99 +274,6 @@ class TemplateResult(str): ) -class TemplatesResultDict(dict): - """Holds and wrap TemplateResults for easy bug report.""" - - def __init__(self, in_data, key=None, parent=None, strict=None): - super(TemplatesResultDict, self).__init__() - for _key, _value in in_data.items(): - if isinstance(_value, dict): - _value = self.__class__(_value, _key, self) - self[_key] = _value - - self.key = key - self.parent = parent - self.strict = strict - if self.parent is None and strict is None: - self.strict = True - - def __getitem__(self, key): - if key not in self.keys(): - hier = self.hierarchy() - hier.append(key) - raise TemplateMissingKey(hier) - - value = super(TemplatesResultDict, self).__getitem__(key) - if isinstance(value, self.__class__): - return value - - # Raise exception when expected solved templates and it is not. - if self.raise_on_unsolved and hasattr(value, "validate"): - value.validate() - return value - - @property - def raise_on_unsolved(self): - """To affect this change `strict` attribute.""" - if self.strict is not None: - return self.strict - return self.parent.raise_on_unsolved - - def hierarchy(self): - """Return dictionary keys one by one to root parent.""" - if self.parent is None: - return [] - - hier_keys = [] - par_hier = self.parent.hierarchy() - if par_hier: - hier_keys.extend(par_hier) - hier_keys.append(self.key) - - return hier_keys - - @property - def missing_keys(self): - """Return missing keys of all children templates.""" - missing_keys = set() - for value in self.values(): - missing_keys |= value.missing_keys - return missing_keys - - @property - def invalid_types(self): - """Return invalid types of all children templates.""" - invalid_types = {} - for value in self.values(): - invalid_types = merge_dict(invalid_types, value.invalid_types) - return invalid_types - - @property - def used_values(self): - """Return used values for all children templates.""" - used_values = {} - for value in self.values(): - used_values = merge_dict(used_values, value.used_values) - return used_values - - def get_solved(self): - """Get only solved key from templates.""" - result = {} - for key, value in self.items(): - if isinstance(value, self.__class__): - value = value.get_solved() - if not value: - continue - result[key] = value - - elif ( - not hasattr(value, "solved") or - value.solved - ): - result[key] = value - return self.__class__(result, key=self.key, parent=self.parent) - - class TemplatePartResult: """Result to store result of template parts.""" def __init__(self, optional=False): diff --git a/client/ayon_core/lib/python_module_tools.py b/client/ayon_core/lib/python_module_tools.py index 4f9eb7f667..cb6e4c14c4 100644 --- a/client/ayon_core/lib/python_module_tools.py +++ b/client/ayon_core/lib/python_module_tools.py @@ -118,8 +118,8 @@ def classes_from_module(superclass, module): Arguments: superclass (superclass): Superclass of subclasses to look for - module (types.ModuleType): Imported module from which to - parse valid Avalon plug-ins. + module (types.ModuleType): Imported module where to look for + 'superclass' subclasses. Returns: List of plug-ins, or empty list if none is found. diff --git a/client/ayon_core/lib/terminal.py b/client/ayon_core/lib/terminal.py index f822a37286..a22f2358aa 100644 --- a/client/ayon_core/lib/terminal.py +++ b/client/ayon_core/lib/terminal.py @@ -69,7 +69,7 @@ class Terminal: Terminal.use_colors = False print( "Module `blessed` failed on import or terminal creation." - " Pype terminal won't use colors." + " AYON terminal won't use colors." ) Terminal._initialized = True return diff --git a/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py b/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py index 985cf49b97..7854f0ceba 100644 --- a/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py +++ b/client/ayon_core/modules/clockify/ftrack/server/action_clockify_sync_server.py @@ -11,7 +11,7 @@ class SyncClockifyServer(ServerAction): label = "Sync To Clockify (server)" description = "Synchronise data to Clockify workspace" - role_list = ["Pypeclub", "Administrator", "project Manager"] + role_list = ["Administrator", "project Manager"] def __init__(self, *args, **kwargs): super(SyncClockifyServer, self).__init__(*args, **kwargs) diff --git a/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py b/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py index 0e8cf6bd37..4701653a0b 100644 --- a/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py +++ b/client/ayon_core/modules/clockify/ftrack/user/action_clockify_sync_local.py @@ -13,7 +13,7 @@ class SyncClockifyLocal(BaseAction): #: Action description. description = 'Synchronise data to Clockify workspace' #: roles that are allowed to register this action - role_list = ["Pypeclub", "Administrator", "project Manager"] + role_list = ["Administrator", "project Manager"] #: icon icon = statics_icon("app_icons", "clockify-white.png") diff --git a/client/ayon_core/modules/deadline/deadline_module.py b/client/ayon_core/modules/deadline/deadline_module.py index d2f0e263d4..c0ba83477e 100644 --- a/client/ayon_core/modules/deadline/deadline_module.py +++ b/client/ayon_core/modules/deadline/deadline_module.py @@ -46,7 +46,6 @@ class DeadlineModule(AYONAddon, IPluginPaths): @staticmethod def get_deadline_pools(webservice, log=None): - # type: (str) -> list """Get pools from Deadline. Args: webservice (str): Server url. diff --git a/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py b/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py index 8123409052..b7ca227b01 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/client/ayon_core/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -9,11 +9,11 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): DL webservice addresses must be configured first in System Settings for project settings enum to work. - Default webservice could be overriden by + Default webservice could be overridden by `project_settings/deadline/deadline_servers`. Currently only single url is expected. - This url could be overriden by some hosts directly on instances with + This url could be overridden by some hosts directly on instances with `CollectDeadlineServerFromInstance`. """ diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py index bc3636da63..1fae23c9b2 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_celaction_deadline.py @@ -74,6 +74,10 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): render_path = os.path.normpath(render_path) script_name = os.path.basename(script_path) + anatomy = instance.context.data["anatomy"] + publish_template = anatomy.get_template_item( + "publish", "default", "path" + ) for item in instance.context: if "workfile" in item.data["productType"]: msg = "Workfile (scene) must be published along" @@ -84,9 +88,9 @@ class CelactionSubmitDeadline(pyblish.api.InstancePlugin): template_data["representation"] = rep template_data["ext"] = rep template_data["comment"] = None - anatomy_filled = instance.context.data["anatomy"].format( - template_data) - template_filled = anatomy_filled["publish"]["path"] + template_filled = publish_template.format_strict( + template_data + ) script_path = os.path.normpath(template_filled) self.log.info( diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py index 837ed91c60..cf124c0bcc 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -123,6 +123,10 @@ class FusionSubmitDeadline( script_path = context.data["currentFile"] + anatomy = instance.context.data["anatomy"] + publish_template = anatomy.get_template_item( + "publish", "default", "path" + ) for item in context: if "workfile" in item.data["families"]: msg = "Workfile (scene) must be published along" @@ -133,8 +137,9 @@ class FusionSubmitDeadline( template_data["representation"] = rep template_data["ext"] = rep template_data["comment"] = None - anatomy_filled = context.data["anatomy"].format(template_data) - template_filled = anatomy_filled["publish"]["path"] + template_filled = publish_template.format_strict( + template_data + ) script_path = os.path.normpath(template_filled) self.log.info( diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py index a3111454b3..ac01af901c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -196,6 +196,11 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, def _get_published_workfile_path(self, context): """This method is temporary while the class is not inherited from AbstractSubmitDeadline""" + anatomy = context.data["anatomy"] + # WARNING Hardcoded template name 'default' > may not be used + publish_template = anatomy.get_template_item( + "publish", "default", "path" + ) for instance in context: if ( instance.data["productType"] != "workfile" @@ -216,11 +221,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, template_data["ext"] = ext template_data["comment"] = None - anatomy = context.data["anatomy"] - # WARNING Hardcoded template name 'publish' > may not be used - template_obj = anatomy.templates_obj["publish"]["path"] - - template_filled = template_obj.format(template_data) + template_filled = publish_template.format(template_data) script_path = os.path.normpath(template_filled) self.log.info( "Using published scene for render {}".format( diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py index 0561e0f65c..50bd414587 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -450,23 +450,10 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, "type": product_type, } - render_templates = anatomy.templates_obj[template_name] - if "folder" in render_templates: - publish_folder = render_templates["folder"].format_strict( - template_data - ) - else: - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(project_name)) - - file_path = render_templates["path"].format_strict(template_data) - publish_folder = os.path.dirname(file_path) - - return publish_folder + render_dir_template = anatomy.get_template_item( + "publish", template_name, "directory" + ) + return render_dir_template.format_strict(template_data) @classmethod def get_attribute_defs(cls): diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py index 7a6abd5507..84bac6d017 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py @@ -573,23 +573,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "type": product_type, } - render_templates = anatomy.templates_obj[template_name] - if "folder" in render_templates: - publish_folder = render_templates["folder"].format_strict( - template_data - ) - else: - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(project_name)) - - file_path = render_templates["path"].format_strict(template_data) - publish_folder = os.path.dirname(file_path) - - return publish_folder + render_dir_template = anatomy.get_template_item( + "publish", template_name, "directory" + ) + return render_dir_template.format_strict(template_data) @classmethod def get_attribute_defs(cls): diff --git a/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index a666c5c2dc..6263526d5c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/client/ayon_core/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -149,7 +149,7 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): """ # no frames in file name at all, eg 'renderCompositingMain.withLut.mov' if not frame_placeholder: - return set([file_name_template]) + return {file_name_template} real_expected_rendered = set() src_padding_exp = "%0{}d".format(len(frame_placeholder)) diff --git a/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py b/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py index 9641c16d20..f146aef7b4 100644 --- a/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py +++ b/client/ayon_core/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py @@ -404,7 +404,7 @@ class OpenPypeTileAssembler(DeadlinePlugin): Args: output_width (int): Width of output image. output_height (int): Height of output image. - tiles_info (list): List of tile items, each item must be + tile_info (list): List of tile items, each item must be dictionary with `filepath`, `pos_x` and `pos_y` keys representing path to file and x, y coordinates on output image where top-left point of tile item should start. diff --git a/client/ayon_core/pipeline/anatomy.py b/client/ayon_core/pipeline/anatomy.py deleted file mode 100644 index d6e09bad39..0000000000 --- a/client/ayon_core/pipeline/anatomy.py +++ /dev/null @@ -1,1550 +0,0 @@ -import os -import re -import copy -import platform -import collections -import numbers -import time - -import six -import ayon_api - -from ayon_core.lib import Logger, get_local_site_id -from ayon_core.lib.path_templates import ( - TemplateUnsolved, - TemplateResult, - StringTemplate, - TemplatesDict, - FormatObject, -) -from ayon_core.addon import AddonsManager - -log = Logger.get_logger(__name__) - - -class ProjectNotSet(Exception): - """Exception raised when is created Anatomy without project name.""" - - -class RootCombinationError(Exception): - """This exception is raised when templates has combined root types.""" - - def __init__(self, roots): - joined_roots = ", ".join( - ["\"{}\"".format(_root) for _root in roots] - ) - # TODO better error message - msg = ( - "Combination of root with and" - " without root name in AnatomyTemplates. {}" - ).format(joined_roots) - - super(RootCombinationError, self).__init__(msg) - - -class BaseAnatomy(object): - """Anatomy module helps to keep project settings. - - Wraps key project specifications, AnatomyTemplates and Roots. - """ - root_key_regex = re.compile(r"{(root?[^}]+)}") - root_name_regex = re.compile(r"root\[([^]]+)\]") - - def __init__(self, project_entity, root_overrides=None): - project_name = project_entity["name"] - self.project_name = project_name - self.project_code = project_entity["code"] - - self._data = self._prepare_anatomy_data( - project_entity, root_overrides - ) - self._templates_obj = AnatomyTemplates(self) - self._roots_obj = Roots(self) - - # Anatomy used as dictionary - # - implemented only getters returning copy - def __getitem__(self, key): - return copy.deepcopy(self._data[key]) - - def get(self, key, default=None): - return copy.deepcopy(self._data).get(key, default) - - def keys(self): - return copy.deepcopy(self._data).keys() - - def values(self): - return copy.deepcopy(self._data).values() - - def items(self): - return copy.deepcopy(self._data).items() - - def _prepare_anatomy_data(self, project_entity, root_overrides): - """Prepare anatomy data for further processing. - - Method added to replace `{task}` with `{task[name]}` in templates. - """ - - anatomy_data = self._project_entity_to_anatomy_data(project_entity) - - self._apply_local_settings_on_anatomy_data( - anatomy_data, - root_overrides - ) - - return anatomy_data - - @property - def templates(self): - """Wrap property `templates` of Anatomy's AnatomyTemplates instance.""" - return self._templates_obj.templates - - @property - def templates_obj(self): - """Return `AnatomyTemplates` object of current Anatomy instance.""" - return self._templates_obj - - def format(self, *args, **kwargs): - """Wrap `format` method of Anatomy's `templates_obj`.""" - return self._templates_obj.format(*args, **kwargs) - - def format_all(self, *args, **kwargs): - """Wrap `format_all` method of Anatomy's `templates_obj`.""" - return self._templates_obj.format_all(*args, **kwargs) - - @property - def roots(self): - """Wrap `roots` property of Anatomy's `roots_obj`.""" - return self._roots_obj.roots - - @property - def roots_obj(self): - """Return `Roots` object of current Anatomy instance.""" - return self._roots_obj - - def root_environments(self): - """Return AYON_PROJECT_ROOT_* environments for current project.""" - return self._roots_obj.root_environments() - - def root_environmets_fill_data(self, template=None): - """Environment variable values in dictionary for rootless path. - - Args: - template (str): Template for environment variable key fill. - By default is set to `"${}"`. - """ - return self.roots_obj.root_environmets_fill_data(template) - - def find_root_template_from_path(self, *args, **kwargs): - """Wrapper for Roots `find_root_template_from_path`.""" - return self.roots_obj.find_root_template_from_path(*args, **kwargs) - - def path_remapper(self, *args, **kwargs): - """Wrapper for Roots `path_remapper`.""" - return self.roots_obj.path_remapper(*args, **kwargs) - - def all_root_paths(self): - """Wrapper for Roots `all_root_paths`.""" - return self.roots_obj.all_root_paths() - - def set_root_environments(self): - """Set AYON_PROJECT_ROOT_* environments for current project.""" - self._roots_obj.set_root_environments() - - def root_names(self): - """Return root names for current project.""" - return self.root_names_from_templates(self.templates) - - def _root_keys_from_templates(self, data): - """Extract root key from templates in data. - - Args: - data (dict): Data that may contain templates as string. - - Return: - set: Set of all root names from templates as strings. - - Output example: `{"root[work]", "root[publish]"}` - """ - - output = set() - if isinstance(data, dict): - for value in data.values(): - for root in self._root_keys_from_templates(value): - output.add(root) - - elif isinstance(data, str): - for group in re.findall(self.root_key_regex, data): - output.add(group) - - return output - - def root_value_for_template(self, template): - """Returns value of root key from template.""" - root_templates = [] - for group in re.findall(self.root_key_regex, template): - root_templates.append("{" + group + "}") - - if not root_templates: - return None - - return root_templates[0].format(**{"root": self.roots}) - - def root_names_from_templates(self, templates): - """Extract root names form anatomy templates. - - Returns None if values in templates contain only "{root}". - Empty list is returned if there is no "root" in templates. - Else returns all root names from templates in list. - - RootCombinationError is raised when templates contain both root types, - basic "{root}" and with root name specification "{root[work]}". - - Args: - templates (dict): Anatomy templates where roots are not filled. - - Return: - list/None: List of all root names from templates as strings when - multiroot setup is used, otherwise None is returned. - """ - roots = list(self._root_keys_from_templates(templates)) - # Return empty list if no roots found in templates - if not roots: - return roots - - # Raise exception when root keys have roots with and without root name. - # Invalid output example: ["root", "root[project]", "root[render]"] - if len(roots) > 1 and "root" in roots: - raise RootCombinationError(roots) - - # Return None if "root" without root name in templates - if len(roots) == 1 and roots[0] == "root": - return None - - names = set() - for root in roots: - for group in re.findall(self.root_name_regex, root): - names.add(group) - return list(names) - - def fill_root(self, template_path): - """Fill template path where is only "root" key unfilled. - - Args: - template_path (str): Path with "root" key in. - Example path: "{root}/projects/MyProject/Shot01/Lighting/..." - - Return: - str: formatted path - """ - # NOTE does not care if there are different keys than "root" - return template_path.format(**{"root": self.roots}) - - @classmethod - def fill_root_with_path(cls, rootless_path, root_path): - """Fill path without filled "root" key with passed path. - - This is helper to fill root with different directory path than anatomy - has defined no matter if is single or multiroot. - - Output path is same as input path if `rootless_path` does not contain - unfilled root key. - - Args: - rootless_path (str): Path without filled "root" key. Example: - "{root[work]}/MyProject/..." - root_path (str): What should replace root key in `rootless_path`. - - Returns: - str: Path with filled root. - """ - output = str(rootless_path) - for group in re.findall(cls.root_key_regex, rootless_path): - replacement = "{" + group + "}" - output = output.replace(replacement, root_path) - - return output - - def replace_root_with_env_key(self, filepath, template=None): - """Replace root of path with environment key. - - # Example: - ## Project with roots: - ``` - { - "nas": { - "windows": P:/projects", - ... - } - ... - } - ``` - - ## Entered filepath - "P:/projects/project/folder/task/animation_v001.ma" - - ## Entered template - "<{}>" - - ## Output - "/project/folder/task/animation_v001.ma" - - Args: - filepath (str): Full file path where root should be replaced. - template (str): Optional template for environment key. Must - have one index format key. - Default value if not entered: "${}" - - Returns: - str: Path where root is replaced with environment root key. - - Raise: - ValueError: When project's roots were not found in entered path. - """ - success, rootless_path = self.find_root_template_from_path(filepath) - if not success: - raise ValueError( - "{}: Project's roots were not found in path: {}".format( - self.project_name, filepath - ) - ) - - data = self.root_environmets_fill_data(template) - return rootless_path.format(**data) - - def _project_entity_to_anatomy_data(self, project_entity): - """Convert project document to anatomy data. - - Probably should fill missing keys and values. - """ - - output = copy.deepcopy(project_entity["config"]) - # TODO remove AYON convertion - task_types = copy.deepcopy(project_entity["taskTypes"]) - new_task_types = {} - for task_type in task_types: - name = task_type["name"] - new_task_types[name] = task_type - output["tasks"] = new_task_types - output["attributes"] = copy.deepcopy(project_entity["attrib"]) - - return output - - def _apply_local_settings_on_anatomy_data( - self, anatomy_data, root_overrides - ): - """Apply local settings on anatomy data. - - ATM local settings can modify project roots. Project name is required - as local settings have data stored data by project's name. - - Local settings override root values in this order: - 1.) Check if local settings contain overrides for default project and - apply it's values on roots if there are any. - 2.) If passed `project_name` is not None then check project specific - overrides in local settings for the project and apply it's value on - roots if there are any. - - NOTE: Root values of default project from local settings are always - applied if are set. - - Args: - anatomy_data (dict): Data for anatomy. - root_overrides (dict): Data of local settings. - """ - - # Skip processing if roots for current active site are not available in - # local settings - if not root_overrides: - return - - current_platform = platform.system().lower() - - root_data = anatomy_data["roots"] - for root_name, path in root_overrides.items(): - if root_name not in root_data: - continue - anatomy_data["roots"][root_name][current_platform] = ( - path - ) - - -class CacheItem: - """Helper to cache data. - - Helper does not handle refresh of data and does not mark data as outdated. - Who uses the object should check of outdated state on his own will. - """ - - default_lifetime = 10 - - def __init__(self, lifetime=None): - self._data = None - self._cached = None - self._lifetime = lifetime or self.default_lifetime - - @property - def data(self): - """Cached data/object. - - Returns: - Any: Whatever was cached. - """ - - return self._data - - @property - def is_outdated(self): - """Item has outdated cache. - - Lifetime of cache item expired or was not yet set. - - Returns: - bool: Item is outdated. - """ - - if self._cached is None: - return True - return (time.time() - self._cached) > self._lifetime - - def update_data(self, data): - """Update cache of data. - - Args: - data (Any): Data to cache. - """ - - self._data = data - self._cached = time.time() - - -class Anatomy(BaseAnatomy): - _sync_server_addon_cache = CacheItem() - _project_cache = collections.defaultdict(CacheItem) - _default_site_id_cache = collections.defaultdict(CacheItem) - _root_overrides_cache = collections.defaultdict( - lambda: collections.defaultdict(CacheItem) - ) - - def __init__( - self, project_name=None, site_name=None, project_entity=None - ): - if not project_name: - project_name = os.environ.get("AYON_PROJECT_NAME") - - if not project_name: - raise ProjectNotSet(( - "Implementation bug: Project name is not set. Anatomy requires" - " to load data for specific project." - )) - - if not project_entity: - project_entity = self.get_project_entity_from_cache(project_name) - root_overrides = self._get_site_root_overrides( - project_name, site_name - ) - - super(Anatomy, self).__init__(project_entity, root_overrides) - - @classmethod - def get_project_entity_from_cache(cls, project_name): - project_cache = cls._project_cache[project_name] - if project_cache.is_outdated: - project_cache.update_data(ayon_api.get_project(project_name)) - return copy.deepcopy(project_cache.data) - - @classmethod - def get_sync_server_addon(cls): - if cls._sync_server_addon_cache.is_outdated: - manager = AddonsManager() - cls._sync_server_addon_cache.update_data( - manager.get_enabled_addon("sync_server") - ) - return cls._sync_server_addon_cache.data - - @classmethod - def _get_studio_roots_overrides(cls, project_name): - """This would return 'studio' site override by local settings. - - Notes: - This logic handles local overrides of studio site which may be - available even when sync server is not enabled. - Handling of 'studio' and 'local' site was separated as preparation - for AYON development where that will be received from - separated sources. - - Args: - project_name (str): Name of project. - - Returns: - Union[Dict[str, str], None]): Local root overrides. - """ - if not project_name: - return - return ayon_api.get_project_roots_for_site( - project_name, get_local_site_id() - ) - - @classmethod - def _get_site_root_overrides(cls, project_name, site_name): - """Get root overrides for site. - - Args: - project_name (str): Project name for which root overrides should be - received. - site_name (Union[str, None]): Name of site for which root overrides - should be returned. - """ - - # First check if sync server is available and enabled - sync_server = cls.get_sync_server_addon() - if sync_server is None or not sync_server.enabled: - # QUESTION is ok to force 'studio' when site sync is not enabled? - site_name = "studio" - - elif not site_name: - # Use sync server to receive active site name - project_cache = cls._default_site_id_cache[project_name] - if project_cache.is_outdated: - project_cache.update_data( - sync_server.get_active_site_type(project_name) - ) - site_name = project_cache.data - - site_cache = cls._root_overrides_cache[project_name][site_name] - if site_cache.is_outdated: - if site_name == "studio": - # Handle studio root overrides without sync server - # - studio root overrides can be done even without sync server - roots_overrides = cls._get_studio_roots_overrides( - project_name - ) - else: - # Ask sync server to get roots overrides - roots_overrides = sync_server.get_site_root_overrides( - project_name, site_name - ) - site_cache.update_data(roots_overrides) - return site_cache.data - - -class AnatomyTemplateUnsolved(TemplateUnsolved): - """Exception for unsolved template when strict is set to True.""" - - msg = "Anatomy template \"{0}\" is unsolved.{1}{2}" - - -class AnatomyTemplateResult(TemplateResult): - rootless = None - - def __new__(cls, result, rootless_path): - new_obj = super(AnatomyTemplateResult, cls).__new__( - cls, - str(result), - result.template, - result.solved, - result.used_values, - result.missing_keys, - result.invalid_types - ) - new_obj.rootless = rootless_path - return new_obj - - def validate(self): - if not self.solved: - raise AnatomyTemplateUnsolved( - self.template, - self.missing_keys, - self.invalid_types - ) - - def copy(self): - tmp = TemplateResult( - str(self), - self.template, - self.solved, - self.used_values, - self.missing_keys, - self.invalid_types - ) - return self.__class__(tmp, self.rootless) - - def normalized(self): - """Convert to normalized path.""" - - tmp = TemplateResult( - os.path.normpath(self), - self.template, - self.solved, - self.used_values, - self.missing_keys, - self.invalid_types - ) - return self.__class__(tmp, self.rootless) - - -class AnatomyStringTemplate(StringTemplate): - """String template which has access to anatomy.""" - - def __init__(self, anatomy_templates, template): - self.anatomy_templates = anatomy_templates - super(AnatomyStringTemplate, self).__init__(template) - - def format(self, data): - """Format template and add 'root' key to data if not available. - - Args: - data (dict[str, Any]): Formatting data for template. - - Returns: - AnatomyTemplateResult: Formatting result. - """ - - anatomy_templates = self.anatomy_templates - if not data.get("root"): - data = copy.deepcopy(data) - data["root"] = anatomy_templates.anatomy.roots - result = StringTemplate.format(self, data) - rootless_path = anatomy_templates.rootless_path_from_result(result) - return AnatomyTemplateResult(result, rootless_path) - - -class AnatomyTemplates(TemplatesDict): - inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") - inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") - - def __init__(self, anatomy): - super(AnatomyTemplates, self).__init__() - self.anatomy = anatomy - self.loaded_project = None - - def reset(self): - self._raw_templates = None - self._templates = None - self._objected_templates = None - - @property - def project_name(self): - return self.anatomy.project_name - - @property - def roots(self): - return self.anatomy.roots - - @property - def templates(self): - self._validate_discovery() - return self._templates - - @property - def objected_templates(self): - self._validate_discovery() - return self._objected_templates - - def _validate_discovery(self): - if self.project_name != self.loaded_project: - self.reset() - - if self._templates is None: - self._discover() - self.loaded_project = self.project_name - - def _format_value(self, value, data): - if isinstance(value, RootItem): - return self._solve_dict(value, data) - return super(AnatomyTemplates, self)._format_value(value, data) - - @staticmethod - def _ayon_template_conversion(templates): - def _convert_template_item(template_item): - # Change 'directory' to 'folder' - if "directory" in template_item: - template_item["folder"] = template_item["directory"] - - if ( - "path" not in template_item - and "file" in template_item - and "folder" in template_item - ): - template_item["path"] = "/".join( - (template_item["folder"], template_item["file"]) - ) - - def _get_default_template_name(templates): - default_template = None - for name, template in templates.items(): - if name == "default": - return "default" - - if default_template is None: - default_template = name - - return default_template - - def _fill_template_category(templates, cat_templates, cat_key): - default_template_name = _get_default_template_name(cat_templates) - for template_name, cat_template in cat_templates.items(): - _convert_template_item(cat_template) - if template_name == default_template_name: - templates[cat_key] = cat_template - else: - new_name = "{}_{}".format(cat_key, template_name) - templates["others"][new_name] = cat_template - - others_templates = templates.pop("others", None) or {} - new_others_templates = {} - templates["others"] = new_others_templates - for name, template in others_templates.items(): - _convert_template_item(template) - new_others_templates[name] = template - - for key in ( - "work", - "publish", - "hero", - ): - cat_templates = templates.pop(key) - _fill_template_category(templates, cat_templates, key) - - delivery_templates = templates.pop("delivery", None) or {} - new_delivery_templates = {} - for name, delivery_template in delivery_templates.items(): - new_delivery_templates[name] = "/".join( - (delivery_template["directory"], delivery_template["file"]) - ) - templates["delivery"] = new_delivery_templates - - def set_templates(self, templates): - if not templates: - self.reset() - return - - templates = copy.deepcopy(templates) - # TODO remove AYON convertion - self._ayon_template_conversion(templates) - - self._raw_templates = copy.deepcopy(templates) - v_queue = collections.deque() - v_queue.append(templates) - while v_queue: - item = v_queue.popleft() - if not isinstance(item, dict): - continue - - for key in tuple(item.keys()): - value = item[key] - if isinstance(value, dict): - v_queue.append(value) - - elif ( - isinstance(value, six.string_types) - and "{task}" in value - ): - item[key] = value.replace("{task}", "{task[name]}") - - solved_templates = self.solve_template_inner_links(templates) - self._templates = solved_templates - self._objected_templates = self.create_objected_templates( - solved_templates - ) - - def _create_template_object(self, template): - return AnatomyStringTemplate(self, template) - - def default_templates(self): - """Return default templates data with solved inner keys.""" - return self.solve_template_inner_links( - self.anatomy["templates"] - ) - - def _discover(self): - """ Loads anatomy templates from yaml. - Default templates are loaded if project is not set or project does - not have set it's own. - TODO: create templates if not exist. - - Returns: - TemplatesResultDict: Contain templates data for current project of - default templates. - """ - - if self.project_name is None: - # QUESTION create project specific if not found? - raise AssertionError(( - "Project \"{0}\" does not have his own templates." - " Trying to use default." - ).format(self.project_name)) - - self.set_templates(self.anatomy["templates"]) - - @classmethod - def replace_inner_keys(cls, matches, value, key_values, key): - """Replacement of inner keys in template values.""" - for match in matches: - anatomy_sub_keys = ( - cls.inner_key_name_pattern.findall(match) - ) - if key in anatomy_sub_keys: - raise ValueError(( - "Unsolvable recursion in inner keys, " - "key: \"{}\" is in his own value." - " Can't determine source, please check Anatomy templates." - ).format(key)) - - for anatomy_sub_key in anatomy_sub_keys: - replace_value = key_values.get(anatomy_sub_key) - if replace_value is None: - raise KeyError(( - "Anatomy templates can't be filled." - " Anatomy key `{0}` has" - " invalid inner key `{1}`." - ).format(key, anatomy_sub_key)) - - if not ( - isinstance(replace_value, numbers.Number) - or isinstance(replace_value, six.string_types) - ): - raise ValueError(( - "Anatomy templates can't be filled." - " Anatomy key `{0}` has" - " invalid inner key `{1}`" - " with value `{2}`." - ).format(key, anatomy_sub_key, str(replace_value))) - - value = value.replace(match, str(replace_value)) - - return value - - @classmethod - def prepare_inner_keys(cls, key_values): - """Check values of inner keys. - - Check if inner key exist in template group and has valid value. - It is also required to avoid infinite loop with unsolvable recursion - when first inner key's value refers to second inner key's value where - first is used. - """ - keys_to_solve = set(key_values.keys()) - while True: - found = False - for key in tuple(keys_to_solve): - value = key_values[key] - - if isinstance(value, six.string_types): - matches = cls.inner_key_pattern.findall(value) - if not matches: - keys_to_solve.remove(key) - continue - - found = True - key_values[key] = cls.replace_inner_keys( - matches, value, key_values, key - ) - continue - - elif not isinstance(value, dict): - keys_to_solve.remove(key) - continue - - subdict_found = False - for _key, _value in tuple(value.items()): - matches = cls.inner_key_pattern.findall(_value) - if not matches: - continue - - subdict_found = True - found = True - key_values[key][_key] = cls.replace_inner_keys( - matches, _value, key_values, - "{}.{}".format(key, _key) - ) - - if not subdict_found: - keys_to_solve.remove(key) - - if not found: - break - - return key_values - - @classmethod - def solve_template_inner_links(cls, templates): - """Solve templates inner keys identified by "{@*}". - - Process is split into 2 parts. - First is collecting all global keys (keys in top hierarchy where value - is not dictionary). All global keys are set for all group keys (keys - in top hierarchy where value is dictionary). Value of a key is not - overridden in group if already contain value for the key. - - In second part all keys with "at" symbol in value are replaced with - value of the key afterward "at" symbol from the group. - - Args: - templates (dict): Raw templates data. - - Example: - templates:: - key_1: "value_1", - key_2: "{@key_1}/{filling_key}" - - group_1: - key_3: "value_3/{@key_2}" - - group_2: - key_2": "value_2" - key_4": "value_4/{@key_2}" - - output:: - key_1: "value_1" - key_2: "value_1/{filling_key}" - - group_1: { - key_1: "value_1" - key_2: "value_1/{filling_key}" - key_3: "value_3/value_1/{filling_key}" - - group_2: { - key_1: "value_1" - key_2: "value_2" - key_4: "value_3/value_2" - """ - default_key_values = templates.pop("common", {}) - for key, value in tuple(templates.items()): - if isinstance(value, dict): - continue - default_key_values[key] = templates.pop(key) - - # Pop "others" key before before expected keys are processed - other_templates = templates.pop("others") or {} - - keys_by_subkey = {} - for sub_key, sub_value in templates.items(): - key_values = {} - key_values.update(default_key_values) - key_values.update(sub_value) - keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - - for sub_key, sub_value in other_templates.items(): - if sub_key in keys_by_subkey: - log.warning(( - "Key \"{}\" is duplicated in others. Skipping." - ).format(sub_key)) - continue - - key_values = {} - key_values.update(default_key_values) - key_values.update(sub_value) - keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - - default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values) - - for key, value in default_keys_by_subkeys.items(): - keys_by_subkey[key] = value - - return keys_by_subkey - - @classmethod - def _dict_to_subkeys_list(cls, subdict, pre_keys=None): - if pre_keys is None: - pre_keys = [] - output = [] - for key in subdict: - value = subdict[key] - result = list(pre_keys) - result.append(key) - if isinstance(value, dict): - for item in cls._dict_to_subkeys_list(value, result): - output.append(item) - else: - output.append(result) - return output - - def _keys_to_dicts(self, key_list, value): - if not key_list: - return None - if len(key_list) == 1: - return {key_list[0]: value} - return {key_list[0]: self._keys_to_dicts(key_list[1:], value)} - - @classmethod - def rootless_path_from_result(cls, result): - """Calculate rootless path from formatting result. - - Args: - result (TemplateResult): Result of StringTemplate formatting. - - Returns: - str: Rootless path if result contains one of anatomy roots. - """ - - used_values = result.used_values - missing_keys = result.missing_keys - template = result.template - invalid_types = result.invalid_types - if ( - "root" not in used_values - or "root" in missing_keys - or "{root" not in template - ): - return - - for invalid_type in invalid_types: - if "root" in invalid_type: - return - - root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]}) - if not root_keys: - return - - output = str(result) - for used_root_keys in root_keys: - if not used_root_keys: - continue - - used_value = used_values - root_key = None - for key in used_root_keys: - used_value = used_value[key] - if root_key is None: - root_key = key - else: - root_key += "[{}]".format(key) - - root_key = "{" + root_key + "}" - output = output.replace(str(used_value), root_key) - - return output - - def format(self, data, strict=True): - copy_data = copy.deepcopy(data) - roots = self.roots - if roots: - copy_data["root"] = roots - result = super(AnatomyTemplates, self).format(copy_data) - result.strict = strict - return result - - def format_all(self, in_data, only_keys=True): - """ Solves templates based on entered data. - - Args: - data (dict): Containing keys to be filled into template. - - Returns: - TemplatesResultDict: Output `TemplateResult` have `strict` - attribute set to False so accessing unfilled keys in templates - won't raise any exceptions. - """ - return self.format(in_data, strict=False) - - -class RootItem(FormatObject): - """Represents one item or roots. - - Holds raw data of root item specification. Raw data contain value - for each platform, but current platform value is used when object - is used for formatting of template. - - Args: - root_raw_data (dict): Dictionary containing root values by platform - names. ["windows", "linux" and "darwin"] - name (str, optional): Root name which is representing. Used with - multi root setup otherwise None value is expected. - parent_keys (list, optional): All dictionary parent keys. Values of - `parent_keys` are used for get full key which RootItem is - representing. Used for replacing root value in path with - formattable key. e.g. parent_keys == ["work"] -> {root[work]} - parent (object, optional): It is expected to be `Roots` object. - Value of `parent` won't affect code logic much. - """ - - def __init__( - self, root_raw_data, name=None, parent_keys=None, parent=None - ): - lowered_platform_keys = {} - for key, value in root_raw_data.items(): - lowered_platform_keys[key.lower()] = value - self.raw_data = lowered_platform_keys - self.cleaned_data = self._clean_roots(lowered_platform_keys) - self.name = name - self.parent_keys = parent_keys or [] - self.parent = parent - - self.available_platforms = list(lowered_platform_keys.keys()) - self.value = lowered_platform_keys.get(platform.system().lower()) - self.clean_value = self.clean_root(self.value) - - def __format__(self, *args, **kwargs): - return self.value.__format__(*args, **kwargs) - - def __str__(self): - return str(self.value) - - def __repr__(self): - return self.__str__() - - def __getitem__(self, key): - if isinstance(key, numbers.Number): - return self.value[key] - - additional_info = "" - if self.parent and self.parent.project_name: - additional_info += " for project \"{}\"".format( - self.parent.project_name - ) - - raise AssertionError( - "Root key \"{}\" is missing{}.".format( - key, additional_info - ) - ) - - def full_key(self): - """Full key value for dictionary formatting in template. - - Returns: - str: Return full replacement key for formatting. This helps when - multiple roots are set. In that case e.g. `"root[work]"` is - returned. - """ - if not self.name: - return "root" - - joined_parent_keys = "".join( - ["[{}]".format(key) for key in self.parent_keys] - ) - return "root{}".format(joined_parent_keys) - - def clean_path(self, path): - """Just replace backslashes with forward slashes.""" - return str(path).replace("\\", "/") - - def clean_root(self, root): - """Makes sure root value does not end with slash.""" - if root: - root = self.clean_path(root) - while root.endswith("/"): - root = root[:-1] - return root - - def _clean_roots(self, raw_data): - """Clean all values of raw root item values.""" - cleaned = {} - for key, value in raw_data.items(): - cleaned[key] = self.clean_root(value) - return cleaned - - def path_remapper(self, path, dst_platform=None, src_platform=None): - """Remap path for specific platform. - - Args: - path (str): Source path which need to be remapped. - dst_platform (str, optional): Specify destination platform - for which remapping should happen. - src_platform (str, optional): Specify source platform. This is - recommended to not use and keep unset until you really want - to use specific platform. - roots (dict/RootItem/None, optional): It is possible to remap - path with different roots then instance where method was - called has. - - Returns: - str/None: When path does not contain known root then - None is returned else returns remapped path with "{root}" - or "{root[]}". - """ - cleaned_path = self.clean_path(path) - if dst_platform: - dst_root_clean = self.cleaned_data.get(dst_platform) - if not dst_root_clean: - key_part = "" - full_key = self.full_key() - if full_key != "root": - key_part += "\"{}\" ".format(full_key) - - log.warning( - "Root {}miss platform \"{}\" definition.".format( - key_part, dst_platform - ) - ) - return None - - if cleaned_path.startswith(dst_root_clean): - return cleaned_path - - if src_platform: - src_root_clean = self.cleaned_data.get(src_platform) - if src_root_clean is None: - log.warning( - "Root \"{}\" miss platform \"{}\" definition.".format( - self.full_key(), src_platform - ) - ) - return None - - if not cleaned_path.startswith(src_root_clean): - return None - - subpath = cleaned_path[len(src_root_clean):] - if dst_platform: - # `dst_root_clean` is used from upper condition - return dst_root_clean + subpath - return self.clean_value + subpath - - result, template = self.find_root_template_from_path(path) - if not result: - return None - - def parent_dict(keys, value): - if not keys: - return value - - key = keys.pop(0) - return {key: parent_dict(keys, value)} - - if dst_platform: - format_value = parent_dict(list(self.parent_keys), dst_root_clean) - else: - format_value = parent_dict(list(self.parent_keys), self.value) - - return template.format(**{"root": format_value}) - - def find_root_template_from_path(self, path): - """Replaces known root value with formattable key in path. - - All platform values are checked for this replacement. - - Args: - path (str): Path where root value should be found. - - Returns: - tuple: Tuple contain 2 values: `success` (bool) and `path` (str). - When success it True then path should contain replaced root - value with formattable key. - - Example: - When input path is:: - "C:/windows/path/root/projects/my_project/file.ext" - - And raw data of item looks like:: - { - "windows": "C:/windows/path/root", - "linux": "/mount/root" - } - - Output will be:: - (True, "{root}/projects/my_project/file.ext") - - If any of raw data value wouldn't match path's root output is:: - (False, "C:/windows/path/root/projects/my_project/file.ext") - """ - result = False - output = str(path) - - mod_path = self.clean_path(path) - for root_os, root_path in self.cleaned_data.items(): - # Skip empty paths - if not root_path: - continue - - _mod_path = mod_path # reset to original cleaned value - if root_os == "windows": - root_path = root_path.lower() - _mod_path = _mod_path.lower() - - if _mod_path.startswith(root_path): - result = True - replacement = "{" + self.full_key() + "}" - output = replacement + mod_path[len(root_path):] - break - - return (result, output) - - -class Roots: - """Object which should be used for formatting "root" key in templates. - - Args: - anatomy Anatomy: Anatomy object created for a specific project. - """ - - env_prefix = "AYON_PROJECT_ROOT" - roots_filename = "roots.json" - - def __init__(self, anatomy): - self.anatomy = anatomy - self.loaded_project = None - self._roots = None - - def __format__(self, *args, **kwargs): - return self.roots.__format__(*args, **kwargs) - - def __getitem__(self, key): - return self.roots[key] - - def reset(self): - """Reset current roots value.""" - self._roots = None - - def path_remapper( - self, path, dst_platform=None, src_platform=None, roots=None - ): - """Remap path for specific platform. - - Args: - path (str): Source path which need to be remapped. - dst_platform (str, optional): Specify destination platform - for which remapping should happen. - src_platform (str, optional): Specify source platform. This is - recommended to not use and keep unset until you really want - to use specific platform. - roots (dict/RootItem/None, optional): It is possible to remap - path with different roots then instance where method was - called has. - - Returns: - str/None: When path does not contain known root then - None is returned else returns remapped path with "{root}" - or "{root[]}". - """ - if roots is None: - roots = self.roots - - if roots is None: - raise ValueError("Roots are not set. Can't find path.") - - if "{root" in path: - path = path.format(**{"root": roots}) - # If `dst_platform` is not specified then return else continue. - if not dst_platform: - return path - - if isinstance(roots, RootItem): - return roots.path_remapper(path, dst_platform, src_platform) - - for _root in roots.values(): - result = self.path_remapper( - path, dst_platform, src_platform, _root - ) - if result is not None: - return result - - def find_root_template_from_path(self, path, roots=None): - """Find root value in entered path and replace it with formatting key. - - Args: - path (str): Source path where root will be searched. - roots (Roots/dict, optional): It is possible to use different - roots than instance where method was triggered has. - - Returns: - tuple: Output contains tuple with bool representing success as - first value and path with or without replaced root with - formatting key as second value. - - Raises: - ValueError: When roots are not entered and can't be loaded. - """ - if roots is None: - log.debug( - "Looking for matching root in path \"{}\".".format(path) - ) - roots = self.roots - - if roots is None: - raise ValueError("Roots are not set. Can't find path.") - - if isinstance(roots, RootItem): - return roots.find_root_template_from_path(path) - - for root_name, _root in roots.items(): - success, result = self.find_root_template_from_path(path, _root) - if success: - log.info("Found match in root \"{}\".".format(root_name)) - return success, result - - log.warning("No matching root was found in current setting.") - return (False, path) - - def set_root_environments(self): - """Set root environments for current project.""" - for key, value in self.root_environments().items(): - os.environ[key] = value - - def root_environments(self): - """Use root keys to create unique keys for environment variables. - - Concatenates prefix "AYON_PROJECT_ROOT_" with root keys to create - unique keys. - - Returns: - dict: Result is `{(str): (str)}` dicitonary where key represents - unique key concatenated by keys and value is root value of - current platform root. - - Example: - With raw root values:: - "work": { - "windows": "P:/projects/work", - "linux": "/mnt/share/projects/work", - "darwin": "/darwin/path/work" - }, - "publish": { - "windows": "P:/projects/publish", - "linux": "/mnt/share/projects/publish", - "darwin": "/darwin/path/publish" - } - - Result on windows platform:: - { - "AYON_PROJECT_ROOT_WORK": "P:/projects/work", - "AYON_PROJECT_ROOT_PUBLISH": "P:/projects/publish" - } - - """ - return self._root_environments() - - def all_root_paths(self, roots=None): - """Return all paths for all roots of all platforms.""" - if roots is None: - roots = self.roots - - output = [] - if isinstance(roots, RootItem): - for value in roots.raw_data.values(): - output.append(value) - return output - - for _roots in roots.values(): - output.extend(self.all_root_paths(_roots)) - return output - - def _root_environments(self, keys=None, roots=None): - if not keys: - keys = [] - if roots is None: - roots = self.roots - - if isinstance(roots, RootItem): - key_items = [self.env_prefix] - for _key in keys: - key_items.append(_key.upper()) - - key = "_".join(key_items) - # Make sure key and value does not contain unicode - # - can happen in Python 2 hosts - return {str(key): str(roots.value)} - - output = {} - for _key, _value in roots.items(): - _keys = list(keys) - _keys.append(_key) - output.update(self._root_environments(_keys, _value)) - return output - - def root_environmets_fill_data(self, template=None): - """Environment variable values in dictionary for rootless path. - - Args: - template (str): Template for environment variable key fill. - By default is set to `"${}"`. - """ - if template is None: - template = "${}" - return self._root_environmets_fill_data(template) - - def _root_environmets_fill_data(self, template, keys=None, roots=None): - if keys is None and roots is None: - return { - "root": self._root_environmets_fill_data( - template, [], self.roots - ) - } - - if isinstance(roots, RootItem): - key_items = [Roots.env_prefix] - for _key in keys: - key_items.append(_key.upper()) - key = "_".join(key_items) - return template.format(key) - - output = {} - for key, value in roots.items(): - _keys = list(keys) - _keys.append(key) - output[key] = self._root_environmets_fill_data( - template, _keys, value - ) - return output - - @property - def project_name(self): - """Return project name which will be used for loading root values.""" - return self.anatomy.project_name - - @property - def roots(self): - """Property for filling "root" key in templates. - - This property returns roots for current project or default root values. - Warning: - Default roots value may cause issues when project use different - roots settings. That may happen when project use multiroot - templates but default roots miss their keys. - """ - if self.project_name != self.loaded_project: - self._roots = None - - if self._roots is None: - self._roots = self._discover() - self.loaded_project = self.project_name - return self._roots - - def _discover(self): - """ Loads current project's roots or default. - - Default roots are loaded if project override's does not contain roots. - - Returns: - `RootItem` or `dict` with multiple `RootItem`s when multiroot - setting is used. - """ - - return self._parse_dict(self.anatomy["roots"], parent=self) - - @staticmethod - def _parse_dict(data, key=None, parent_keys=None, parent=None): - """Parse roots raw data into RootItem or dictionary with RootItems. - - Converting raw roots data to `RootItem` helps to handle platform keys. - This method is recursive to be able handle multiroot setup and - is static to be able to load default roots without creating new object. - - Args: - data (dict): Should contain raw roots data to be parsed. - key (str, optional): Current root key. Set by recursion. - parent_keys (list): Parent dictionary keys. Set by recursion. - parent (Roots, optional): Parent object set in `RootItem` - helps to keep RootItem instance updated with `Roots` object. - - Returns: - `RootItem` or `dict` with multiple `RootItem`s when multiroot - setting is used. - """ - if not parent_keys: - parent_keys = [] - is_last = False - for value in data.values(): - if isinstance(value, six.string_types): - is_last = True - break - - if is_last: - return RootItem(data, key, parent_keys, parent=parent) - - output = {} - for _key, value in data.items(): - _parent_keys = list(parent_keys) - _parent_keys.append(_key) - output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent) - return output diff --git a/client/ayon_core/pipeline/anatomy/__init__.py b/client/ayon_core/pipeline/anatomy/__init__.py new file mode 100644 index 0000000000..336d09ccaa --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/__init__.py @@ -0,0 +1,17 @@ +from .exceptions import ( + ProjectNotSet, + RootCombinationError, + TemplateMissingKey, + AnatomyTemplateUnsolved, +) +from .anatomy import Anatomy + + +__all__ = ( + "ProjectNotSet", + "RootCombinationError", + "TemplateMissingKey", + "AnatomyTemplateUnsolved", + + "Anatomy", +) diff --git a/client/ayon_core/pipeline/anatomy/anatomy.py b/client/ayon_core/pipeline/anatomy/anatomy.py new file mode 100644 index 0000000000..0d250116bd --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/anatomy.py @@ -0,0 +1,556 @@ +import os +import re +import copy +import platform +import collections +import time + +import ayon_api + +from ayon_core.lib import Logger, get_local_site_id, StringTemplate +from ayon_core.addon import AddonsManager + +from .exceptions import RootCombinationError, ProjectNotSet +from .roots import AnatomyRoots +from .templates import AnatomyTemplates + +log = Logger.get_logger(__name__) + + +class BaseAnatomy(object): + """Anatomy module helps to keep project settings. + + Wraps key project specifications, AnatomyTemplates and AnatomyRoots. + """ + root_key_regex = re.compile(r"{(root?[^}]+)}") + root_name_regex = re.compile(r"root\[([^]]+)\]") + + def __init__(self, project_entity, root_overrides=None): + self._project_name = project_entity["name"] + self._project_code = project_entity["code"] + + self._data = self._prepare_anatomy_data( + project_entity, root_overrides + ) + self._templates_obj = AnatomyTemplates(self) + self._roots_obj = AnatomyRoots(self) + + # Anatomy used as dictionary + # - implemented only getters returning copy + def __getitem__(self, key): + return copy.deepcopy(self._data[key]) + + def get(self, key, default=None): + if key not in self._data: + return default + return copy.deepcopy(self._data[key]) + + def keys(self): + return copy.deepcopy(self._data).keys() + + def values(self): + return copy.deepcopy(self._data).values() + + def items(self): + return copy.deepcopy(self._data).items() + + @property + def project_name(self): + """Project name for which is anatomy prepared. + + Returns: + str: Project name. + + """ + return self._project_name + + @property + def project_code(self): + """Project name for which is anatomy prepared. + + Returns: + str: Project code. + + """ + return self._project_code + + def _prepare_anatomy_data(self, project_entity, root_overrides): + """Prepare anatomy data for further processing. + + Method added to replace `{task}` with `{task[name]}` in templates. + """ + + anatomy_data = self._project_entity_to_anatomy_data(project_entity) + + self._apply_local_settings_on_anatomy_data( + anatomy_data, + root_overrides + ) + + return anatomy_data + + @property + def templates(self): + """Wrap property `templates` of Anatomy's AnatomyTemplates instance.""" + return self._templates_obj.templates + + @property + def templates_obj(self): + """Return `AnatomyTemplates` object of current Anatomy instance.""" + return self._templates_obj + + def get_template_item(self, *args, **kwargs): + """Get template item from category. + + Args: + category_name (str): Category name. + template_name (str): Template name. + subkey (Optional[str]): Subkey name. + default (Any): Default value. + + Returns: + Any: Template item, subkey value as AnatomyStringTemplate or None. + + """ + return self._templates_obj.get_template_item(*args, **kwargs) + + def format(self, *args, **kwargs): + """Wrap `format` method of Anatomy's `templates_obj`.""" + return self._templates_obj.format(*args, **kwargs) + + def format_all(self, *args, **kwargs): + """Wrap `format_all` method of Anatomy's `templates_obj`. + + Deprecated: + Use ``format`` method with ``strict=False`` instead. + + """ + return self._templates_obj.format_all(*args, **kwargs) + + @property + def roots(self): + """Wrap `roots` property of Anatomy's `roots_obj`.""" + return self._roots_obj.roots + + @property + def roots_obj(self): + """Roots wrapper object. + + Returns: + AnatomyRoots: Roots wrapper. + + """ + return self._roots_obj + + def root_environments(self): + """Return AYON_PROJECT_ROOT_* environments for current project.""" + return self._roots_obj.root_environments() + + def root_environmets_fill_data(self, template=None): + """Environment variable values in dictionary for rootless path. + + Args: + template (str): Template for environment variable key fill. + By default is set to `"${}"`. + """ + return self.roots_obj.root_environmets_fill_data(template) + + def find_root_template_from_path(self, *args, **kwargs): + """Wrapper for AnatomyRoots `find_root_template_from_path`.""" + return self.roots_obj.find_root_template_from_path(*args, **kwargs) + + def path_remapper(self, *args, **kwargs): + """Wrapper for AnatomyRoots `path_remapper`.""" + return self.roots_obj.path_remapper(*args, **kwargs) + + def all_root_paths(self): + """Wrapper for AnatomyRoots `all_root_paths`.""" + return self.roots_obj.all_root_paths() + + def set_root_environments(self): + """Set AYON_PROJECT_ROOT_* environments for current project.""" + self._roots_obj.set_root_environments() + + def root_names(self): + """Return root names for current project.""" + return self.root_names_from_templates(self.templates) + + def _root_keys_from_templates(self, data): + """Extract root key from templates in data. + + Args: + data (dict): Data that may contain templates as string. + + Return: + set: Set of all root names from templates as strings. + + Output example: `{"root[work]", "root[publish]"}` + """ + + output = set() + keys_queue = collections.deque() + keys_queue.append(data) + while keys_queue: + queue_data = keys_queue.popleft() + if isinstance(queue_data, StringTemplate): + queue_data = queue_data.template + + if isinstance(queue_data, dict): + for value in queue_data.values(): + keys_queue.append(value) + + elif isinstance(queue_data, str): + for group in re.findall(self.root_key_regex, queue_data): + output.add(group) + + return output + + def root_value_for_template(self, template): + """Returns value of root key from template.""" + if isinstance(template, StringTemplate): + template = template.template + root_templates = [] + for group in re.findall(self.root_key_regex, template): + root_templates.append("{" + group + "}") + + if not root_templates: + return None + + return root_templates[0].format(**{"root": self.roots}) + + def root_names_from_templates(self, templates): + """Extract root names form anatomy templates. + + Returns None if values in templates contain only "{root}". + Empty list is returned if there is no "root" in templates. + Else returns all root names from templates in list. + + RootCombinationError is raised when templates contain both root types, + basic "{root}" and with root name specification "{root[work]}". + + Args: + templates (dict): Anatomy templates where roots are not filled. + + Return: + list/None: List of all root names from templates as strings when + multiroot setup is used, otherwise None is returned. + """ + roots = list(self._root_keys_from_templates(templates)) + # Return empty list if no roots found in templates + if not roots: + return roots + + # Raise exception when root keys have roots with and without root name. + # Invalid output example: ["root", "root[project]", "root[render]"] + if len(roots) > 1 and "root" in roots: + raise RootCombinationError(roots) + + # Return None if "root" without root name in templates + if len(roots) == 1 and roots[0] == "root": + return None + + names = set() + for root in roots: + for group in re.findall(self.root_name_regex, root): + names.add(group) + return list(names) + + def fill_root(self, template_path): + """Fill template path where is only "root" key unfilled. + + Args: + template_path (str): Path with "root" key in. + Example path: "{root}/projects/MyProject/Shot01/Lighting/..." + + Return: + str: formatted path + """ + # NOTE does not care if there are different keys than "root" + return template_path.format(**{"root": self.roots}) + + @classmethod + def fill_root_with_path(cls, rootless_path, root_path): + """Fill path without filled "root" key with passed path. + + This is helper to fill root with different directory path than anatomy + has defined no matter if is single or multiroot. + + Output path is same as input path if `rootless_path` does not contain + unfilled root key. + + Args: + rootless_path (str): Path without filled "root" key. Example: + "{root[work]}/MyProject/..." + root_path (str): What should replace root key in `rootless_path`. + + Returns: + str: Path with filled root. + """ + output = str(rootless_path) + for group in re.findall(cls.root_key_regex, rootless_path): + replacement = "{" + group + "}" + output = output.replace(replacement, root_path) + + return output + + def replace_root_with_env_key(self, filepath, template=None): + """Replace root of path with environment key. + + # Example: + ## Project with roots: + ``` + { + "nas": { + "windows": P:/projects", + ... + } + ... + } + ``` + + ## Entered filepath + "P:/projects/project/folder/task/animation_v001.ma" + + ## Entered template + "<{}>" + + ## Output + "/project/folder/task/animation_v001.ma" + + Args: + filepath (str): Full file path where root should be replaced. + template (str): Optional template for environment key. Must + have one index format key. + Default value if not entered: "${}" + + Returns: + str: Path where root is replaced with environment root key. + + Raise: + ValueError: When project's roots were not found in entered path. + """ + success, rootless_path = self.find_root_template_from_path(filepath) + if not success: + raise ValueError( + "{}: Project's roots were not found in path: {}".format( + self.project_name, filepath + ) + ) + + data = self.root_environmets_fill_data(template) + return rootless_path.format(**data) + + def _project_entity_to_anatomy_data(self, project_entity): + """Convert project document to anatomy data. + + Probably should fill missing keys and values. + """ + + output = copy.deepcopy(project_entity["config"]) + # TODO remove AYON convertion + task_types = copy.deepcopy(project_entity["taskTypes"]) + new_task_types = {} + for task_type in task_types: + name = task_type["name"] + new_task_types[name] = task_type + output["tasks"] = new_task_types + output["attributes"] = copy.deepcopy(project_entity["attrib"]) + + return output + + def _apply_local_settings_on_anatomy_data( + self, anatomy_data, root_overrides + ): + """Apply local settings on anatomy data. + + ATM local settings can modify project roots. Project name is required + as local settings have data stored data by project's name. + + Local settings override root values in this order: + 1.) Check if local settings contain overrides for default project and + apply it's values on roots if there are any. + 2.) If passed `project_name` is not None then check project specific + overrides in local settings for the project and apply it's value on + roots if there are any. + + NOTE: Root values of default project from local settings are always + applied if are set. + + Args: + anatomy_data (dict): Data for anatomy. + root_overrides (dict): Data of local settings. + """ + + # Skip processing if roots for current active site are not available in + # local settings + if not root_overrides: + return + + current_platform = platform.system().lower() + + root_data = anatomy_data["roots"] + for root_name, path in root_overrides.items(): + if root_name not in root_data: + continue + anatomy_data["roots"][root_name][current_platform] = ( + path + ) + + +class CacheItem: + """Helper to cache data. + + Helper does not handle refresh of data and does not mark data as outdated. + Who uses the object should check of outdated state on his own will. + """ + + default_lifetime = 10 + + def __init__(self, lifetime=None): + self._data = None + self._cached = None + self._lifetime = lifetime or self.default_lifetime + + @property + def data(self): + """Cached data/object. + + Returns: + Any: Whatever was cached. + """ + + return self._data + + @property + def is_outdated(self): + """Item has outdated cache. + + Lifetime of cache item expired or was not yet set. + + Returns: + bool: Item is outdated. + """ + + if self._cached is None: + return True + return (time.time() - self._cached) > self._lifetime + + def update_data(self, data): + """Update cache of data. + + Args: + data (Any): Data to cache. + """ + + self._data = data + self._cached = time.time() + + +class Anatomy(BaseAnatomy): + _sync_server_addon_cache = CacheItem() + _project_cache = collections.defaultdict(CacheItem) + _default_site_id_cache = collections.defaultdict(CacheItem) + _root_overrides_cache = collections.defaultdict( + lambda: collections.defaultdict(CacheItem) + ) + + def __init__( + self, project_name=None, site_name=None, project_entity=None + ): + if not project_name: + project_name = os.environ.get("AYON_PROJECT_NAME") + + if not project_name: + raise ProjectNotSet(( + "Implementation bug: Project name is not set. Anatomy requires" + " to load data for specific project." + )) + + if not project_entity: + project_entity = self.get_project_entity_from_cache(project_name) + root_overrides = self._get_site_root_overrides( + project_name, site_name + ) + + super(Anatomy, self).__init__(project_entity, root_overrides) + + @classmethod + def get_project_entity_from_cache(cls, project_name): + project_cache = cls._project_cache[project_name] + if project_cache.is_outdated: + project_cache.update_data(ayon_api.get_project(project_name)) + return copy.deepcopy(project_cache.data) + + @classmethod + def get_sync_server_addon(cls): + if cls._sync_server_addon_cache.is_outdated: + manager = AddonsManager() + cls._sync_server_addon_cache.update_data( + manager.get_enabled_addon("sync_server") + ) + return cls._sync_server_addon_cache.data + + @classmethod + def _get_studio_roots_overrides(cls, project_name): + """This would return 'studio' site override by local settings. + + Notes: + This logic handles local overrides of studio site which may be + available even when sync server is not enabled. + Handling of 'studio' and 'local' site was separated as preparation + for AYON development where that will be received from + separated sources. + + Args: + project_name (str): Name of project. + + Returns: + Union[Dict[str, str], None]): Local root overrides. + """ + if not project_name: + return + return ayon_api.get_project_roots_for_site( + project_name, get_local_site_id() + ) + + @classmethod + def _get_site_root_overrides(cls, project_name, site_name): + """Get root overrides for site. + + Args: + project_name (str): Project name for which root overrides should be + received. + site_name (Union[str, None]): Name of site for which root overrides + should be returned. + """ + + # First check if sync server is available and enabled + sync_server = cls.get_sync_server_addon() + if sync_server is None or not sync_server.enabled: + # QUESTION is ok to force 'studio' when site sync is not enabled? + site_name = "studio" + + elif not site_name: + # Use sync server to receive active site name + project_cache = cls._default_site_id_cache[project_name] + if project_cache.is_outdated: + project_cache.update_data( + sync_server.get_active_site_type(project_name) + ) + site_name = project_cache.data + + site_cache = cls._root_overrides_cache[project_name][site_name] + if site_cache.is_outdated: + if site_name == "studio": + # Handle studio root overrides without sync server + # - studio root overrides can be done even without sync server + roots_overrides = cls._get_studio_roots_overrides( + project_name + ) + else: + # Ask sync server to get roots overrides + roots_overrides = sync_server.get_site_root_overrides( + project_name, site_name + ) + site_cache.update_data(roots_overrides) + return site_cache.data diff --git a/client/ayon_core/pipeline/anatomy/exceptions.py b/client/ayon_core/pipeline/anatomy/exceptions.py new file mode 100644 index 0000000000..39f116baf0 --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/exceptions.py @@ -0,0 +1,39 @@ +from ayon_core.lib.path_templates import TemplateUnsolved + + +class ProjectNotSet(Exception): + """Exception raised when is created Anatomy without project name.""" + + +class RootCombinationError(Exception): + """This exception is raised when templates has combined root types.""" + + def __init__(self, roots): + joined_roots = ", ".join( + ["\"{}\"".format(_root) for _root in roots] + ) + # TODO better error message + msg = ( + "Combination of root with and" + " without root name in AnatomyTemplates. {}" + ).format(joined_roots) + + super(RootCombinationError, self).__init__(msg) + + +class TemplateMissingKey(Exception): + """Exception for cases when key does not exist in template.""" + + msg = "Template key '{}' was not found." + + def __init__(self, parents): + parent_join = "".join(["[\"{0}\"]".format(key) for key in parents]) + super(TemplateMissingKey, self).__init__( + self.msg.format(parent_join) + ) + + +class AnatomyTemplateUnsolved(TemplateUnsolved): + """Exception for unsolved template when strict is set to True.""" + + msg = "Anatomy template \"{0}\" is unsolved.{1}{2}" diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py new file mode 100644 index 0000000000..2773559d49 --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -0,0 +1,524 @@ +import os +import platform +import numbers + +from ayon_core.lib import Logger +from ayon_core.lib.path_templates import FormatObject + + +class RootItem(FormatObject): + """Represents one item or roots. + + Holds raw data of root item specification. Raw data contain value + for each platform, but current platform value is used when object + is used for formatting of template. + + Args: + parent (AnatomyRoots): Parent object. + root_raw_data (dict): Dictionary containing root values by platform + names. ["windows", "linux" and "darwin"] + name (str): Root name which is representing. Used with + multi root setup otherwise None value is expected. + """ + def __init__(self, parent, root_raw_data, name): + super(RootItem, self).__init__() + self._log = None + lowered_platform_keys = {} + for key, value in root_raw_data.items(): + lowered_platform_keys[key.lower()] = value + self.raw_data = lowered_platform_keys + self.cleaned_data = self._clean_roots(lowered_platform_keys) + self.name = name + self.parent = parent + + self.available_platforms = set(lowered_platform_keys.keys()) + self.value = lowered_platform_keys.get(platform.system().lower()) + self.clean_value = self._clean_root(self.value) + + def __format__(self, *args, **kwargs): + return self.value.__format__(*args, **kwargs) + + def __str__(self): + return str(self.value) + + def __repr__(self): + return self.__str__() + + def __getitem__(self, key): + if isinstance(key, numbers.Number): + return self.value[key] + + additional_info = "" + if self.parent and self.parent.project_name: + additional_info += " for project \"{}\"".format( + self.parent.project_name + ) + + raise KeyError( + "Root key \"{}\" is missing{}.".format( + key, additional_info + ) + ) + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + @property + def full_key(self): + """Full key value for dictionary formatting in template. + + Returns: + str: Return full replacement key for formatting. This helps when + multiple roots are set. In that case e.g. `"root[work]"` is + returned. + + """ + return "root[{}]".format(self.name) + + @staticmethod + def _clean_path(path): + """Just replace backslashes with forward slashes. + + Args: + path (str): Path which should be cleaned. + + Returns: + str: Cleaned path with forward slashes. + + """ + return str(path).replace("\\", "/") + + def _clean_root(self, root): + """Clean root value. + + Args: + root (str): Root value which should be cleaned. + + Returns: + str: Cleaned root value. + + """ + return self._clean_path(root).rstrip("/") + + def _clean_roots(self, raw_data): + """Clean all values of raw root item values.""" + cleaned = {} + for key, value in raw_data.items(): + cleaned[key] = self._clean_root(value) + return cleaned + + def path_remapper(self, path, dst_platform=None, src_platform=None): + """Remap path for specific platform. + + Args: + path (str): Source path which need to be remapped. + dst_platform (str, optional): Specify destination platform + for which remapping should happen. + src_platform (str, optional): Specify source platform. This is + recommended to not use and keep unset until you really want + to use specific platform. + + Returns: + Union[str, None]: When path does not contain known root then + None is returned else returns remapped path with + "{root[]}". + + """ + cleaned_path = self._clean_path(path) + if dst_platform: + dst_root_clean = self.cleaned_data.get(dst_platform) + if not dst_root_clean: + self.log.warning( + "Root \"{}\" miss platform \"{}\" definition.".format( + self.full_key, dst_platform + ) + ) + return None + + if cleaned_path.startswith(dst_root_clean): + return cleaned_path + + if src_platform: + src_root_clean = self.cleaned_data.get(src_platform) + if src_root_clean is None: + self.log.warning( + "Root \"{}\" miss platform \"{}\" definition.".format( + self.full_key, src_platform + ) + ) + return None + + if not cleaned_path.startswith(src_root_clean): + return None + + subpath = cleaned_path[len(src_root_clean):] + if dst_platform: + # `dst_root_clean` is used from upper condition + return dst_root_clean + subpath + return self.clean_value + subpath + + result, template = self.find_root_template_from_path(path) + if not result: + return None + + if dst_platform: + fill_data = {self.name: dst_root_clean} + else: + fill_data = {self.name: self.value} + + return template.format(**{"root": fill_data}) + + def find_root_template_from_path(self, path): + """Replaces known root value with formattable key in path. + + All platform values are checked for this replacement. + + Args: + path (str): Path where root value should be found. + + Returns: + tuple: Tuple contain 2 values: `success` (bool) and `path` (str). + When success it True then path should contain replaced root + value with formattable key. + + Example: + When input path is:: + "C:/windows/path/root/projects/my_project/file.ext" + + And raw data of item looks like:: + { + "windows": "C:/windows/path/root", + "linux": "/mount/root" + } + + Output will be:: + (True, "{root}/projects/my_project/file.ext") + + If any of raw data value wouldn't match path's root output is:: + (False, "C:/windows/path/root/projects/my_project/file.ext") + """ + result = False + output = str(path) + + mod_path = self._clean_path(path) + for root_os, root_path in self.cleaned_data.items(): + # Skip empty paths + if not root_path: + continue + + _mod_path = mod_path # reset to original cleaned value + if root_os == "windows": + root_path = root_path.lower() + _mod_path = _mod_path.lower() + + if _mod_path.startswith(root_path): + result = True + replacement = "{" + self.full_key + "}" + output = replacement + mod_path[len(root_path):] + break + + return (result, output) + + +class AnatomyRoots: + """Object which should be used for formatting "root" key in templates. + + Args: + anatomy (Anatomy): Anatomy object created for a specific project. + """ + + env_prefix = "AYON_PROJECT_ROOT" + + def __init__(self, anatomy): + self._log = None + self._anatomy = anatomy + self._loaded_project = None + self._roots = None + + def __format__(self, *args, **kwargs): + return self.roots.__format__(*args, **kwargs) + + def __getitem__(self, key): + return self.roots[key] + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + @property + def anatomy(self): + """Parent Anatomy object. + + Returns: + Anatomy: Parent anatomy object. + + """ + return self._anatomy + + def reset(self): + """Reset current roots value.""" + self._roots = None + + def path_remapper( + self, path, dst_platform=None, src_platform=None, roots=None + ): + """Remap path for specific platform. + + Args: + path (str): Source path which need to be remapped. + dst_platform (Optional[str]): Specify destination platform + for which remapping should happen. + src_platform (Optional[str]): Specify source platform. This is + recommended to not use and keep unset until you really want + to use specific platform. + roots (Optional[Union[dict, RootItem])): It is possible to remap + path with different roots then instance where method was + called has. + + Returns: + Union[str, None]: When path does not contain known root then + None is returned else returns remapped path with "{root}" + or "{root[]}". + + """ + if roots is None: + roots = self.roots + + if roots is None: + raise ValueError("Roots are not set. Can't find path.") + + if "{root" in path: + path = path.format(**{"root": roots}) + # If `dst_platform` is not specified then return else continue. + if not dst_platform: + return path + + if isinstance(roots, RootItem): + return roots.path_remapper(path, dst_platform, src_platform) + + for _root in roots.values(): + result = self.path_remapper( + path, dst_platform, src_platform, _root + ) + if result is not None: + return result + + def find_root_template_from_path(self, path, roots=None): + """Find root value in entered path and replace it with formatting key. + + Args: + path (str): Source path where root will be searched. + roots (Optional[Union[AnatomyRoots, dict]): It is possible to use + different roots than instance where method was triggered has. + + Returns: + tuple: Output contains tuple with bool representing success as + first value and path with or without replaced root with + formatting key as second value. + + Raises: + ValueError: When roots are not entered and can't be loaded. + """ + if roots is None: + self.log.debug( + "Looking for matching root in path \"{}\".".format(path) + ) + roots = self.roots + + if roots is None: + raise ValueError("Roots are not set. Can't find path.") + + if isinstance(roots, RootItem): + return roots.find_root_template_from_path(path) + + for root_name, _root in roots.items(): + success, result = self.find_root_template_from_path(path, _root) + if success: + self.log.debug( + "Found match in root \"{}\".".format(root_name) + ) + return success, result + + self.log.warning("No matching root was found in current setting.") + return (False, path) + + def set_root_environments(self): + """Set root environments for current project.""" + for key, value in self.root_environments().items(): + os.environ[key] = value + + def root_environments(self): + """Use root keys to create unique keys for environment variables. + + Concatenates prefix "AYON_PROJECT_ROOT_" with root keys to create + unique keys. + + Returns: + dict: Result is `{(str): (str)}` dicitonary where key represents + unique key concatenated by keys and value is root value of + current platform root. + + Example: + With raw root values:: + "work": { + "windows": "P:/projects/work", + "linux": "/mnt/share/projects/work", + "darwin": "/darwin/path/work" + }, + "publish": { + "windows": "P:/projects/publish", + "linux": "/mnt/share/projects/publish", + "darwin": "/darwin/path/publish" + } + + Result on windows platform:: + { + "AYON_PROJECT_ROOT_WORK": "P:/projects/work", + "AYON_PROJECT_ROOT_PUBLISH": "P:/projects/publish" + } + + """ + return self._root_environments() + + def all_root_paths(self, roots=None): + """Return all paths for all roots of all platforms.""" + if roots is None: + roots = self.roots + + output = [] + if isinstance(roots, RootItem): + for value in roots.raw_data.values(): + output.append(value) + return output + + for _roots in roots.values(): + output.extend(self.all_root_paths(_roots)) + return output + + def _root_environments(self, keys=None, roots=None): + if not keys: + keys = [] + if roots is None: + roots = self.roots + + if isinstance(roots, RootItem): + key_items = [self.env_prefix] + for _key in keys: + key_items.append(_key.upper()) + + key = "_".join(key_items) + # Make sure key and value does not contain unicode + # - can happen in Python 2 hosts + return {str(key): str(roots.value)} + + output = {} + for _key, _value in roots.items(): + _keys = list(keys) + _keys.append(_key) + output.update(self._root_environments(_keys, _value)) + return output + + def root_environmets_fill_data(self, template=None): + """Environment variable values in dictionary for rootless path. + + Args: + template (str): Template for environment variable key fill. + By default is set to `"${}"`. + """ + if template is None: + template = "${}" + return self._root_environmets_fill_data(template) + + def _root_environmets_fill_data(self, template, keys=None, roots=None): + if keys is None and roots is None: + return { + "root": self._root_environmets_fill_data( + template, [], self.roots + ) + } + + if isinstance(roots, RootItem): + key_items = [AnatomyRoots.env_prefix] + for _key in keys: + key_items.append(_key.upper()) + key = "_".join(key_items) + return template.format(key) + + output = {} + for key, value in roots.items(): + _keys = list(keys) + _keys.append(key) + output[key] = self._root_environmets_fill_data( + template, _keys, value + ) + return output + + @property + def project_name(self): + """Current project name which will be used for loading root values. + + Returns: + str: Project name. + """ + return self._anatomy.project_name + + @property + def roots(self): + """Property for filling "root" key in templates. + + This property returns roots for current project or default root values. + + Warning: + Default roots value may cause issues when project use different + roots settings. That may happen when project use multiroot + templates but default roots miss their keys. + + """ + if self.project_name != self._loaded_project: + self._roots = None + + if self._roots is None: + self._roots = self._discover() + self._loaded_project = self.project_name + return self._roots + + def _discover(self): + """ Loads current project's roots or default. + + Default roots are loaded if project override's does not contain roots. + + Returns: + `RootItem` or `dict` with multiple `RootItem`s when multiroot + setting is used. + """ + + return self._parse_dict(self._anatomy["roots"], self) + + @staticmethod + def _parse_dict(data, parent): + """Parse roots raw data into RootItem or dictionary with RootItems. + + Converting raw roots data to `RootItem` helps to handle platform keys. + This method is recursive to be able handle multiroot setup and + is static to be able to load default roots without creating new object. + + Args: + data (dict): Should contain raw roots data to be parsed. + parent (AnatomyRoots): Parent object set as parent + for ``RootItem``. + + Returns: + dict[str, RootItem]: Root items by name. + + """ + output = {} + for root_name, root_values in data.items(): + output[root_name] = RootItem( + parent, root_values, root_name + ) + return output diff --git a/client/ayon_core/pipeline/anatomy/templates.py b/client/ayon_core/pipeline/anatomy/templates.py new file mode 100644 index 0000000000..46cad385f0 --- /dev/null +++ b/client/ayon_core/pipeline/anatomy/templates.py @@ -0,0 +1,891 @@ +import os +import re +import copy +import collections +import numbers + +from ayon_core.lib.path_templates import ( + TemplateResult, + StringTemplate, +) + +from .exceptions import ( + ProjectNotSet, + TemplateMissingKey, + AnatomyTemplateUnsolved, +) +from .roots import RootItem + +_PLACEHOLDER = object() + + +class AnatomyTemplateResult(TemplateResult): + rootless = None + + def __new__(cls, result, rootless_path): + new_obj = super(AnatomyTemplateResult, cls).__new__( + cls, + str(result), + result.template, + result.solved, + result.used_values, + result.missing_keys, + result.invalid_types + ) + new_obj.rootless = rootless_path + return new_obj + + def validate(self): + if not self.solved: + raise AnatomyTemplateUnsolved( + self.template, + self.missing_keys, + self.invalid_types + ) + + def copy(self): + tmp = TemplateResult( + str(self), + self.template, + self.solved, + self.used_values, + self.missing_keys, + self.invalid_types + ) + return self.__class__(tmp, self.rootless) + + def normalized(self): + """Convert to normalized path.""" + + tmp = TemplateResult( + os.path.normpath(self), + self.template, + self.solved, + self.used_values, + self.missing_keys, + self.invalid_types + ) + return self.__class__(tmp, self.rootless) + + +class AnatomyStringTemplate(StringTemplate): + """String template which has access to anatomy. + + Args: + anatomy_templates (AnatomyTemplates): Anatomy templates object. + template (str): Template string. + """ + + def __init__(self, anatomy_templates, template): + self.anatomy_templates = anatomy_templates + super(AnatomyStringTemplate, self).__init__(template) + + def format(self, data): + """Format template and add 'root' key to data if not available. + + Args: + data (dict[str, Any]): Formatting data for template. + + Returns: + AnatomyTemplateResult: Formatting result. + """ + + anatomy_templates = self.anatomy_templates + if not data.get("root"): + data = copy.deepcopy(data) + data["root"] = anatomy_templates.anatomy.roots + result = StringTemplate.format(self, data) + rootless_path = anatomy_templates.get_rootless_path_from_result( + result + ) + return AnatomyTemplateResult(result, rootless_path) + + +def _merge_dict(main_dict, enhance_dict): + """Merges dictionaries by keys. + + Function call itself if value on key is again dictionary. + + Args: + main_dict (dict): First dict to merge second one into. + enhance_dict (dict): Second dict to be merged. + + Returns: + dict: Merged result. + + .. note:: does not override whole value on first found key + but only values differences from enhance_dict + + """ + + merge_queue = collections.deque() + merge_queue.append((main_dict, enhance_dict)) + while merge_queue: + queue_item = merge_queue.popleft() + l_dict, r_dict = queue_item + + for key, value in r_dict.items(): + if key not in l_dict: + l_dict[key] = value + elif isinstance(value, dict) and isinstance(l_dict[key], dict): + merge_queue.append((l_dict[key], value)) + else: + l_dict[key] = value + return main_dict + + +class TemplatesResultDict(dict): + """Holds and wrap 'AnatomyTemplateResult' for easy bug report. + + Dictionary like object which holds 'AnatomyTemplateResult' in the same + data structure as base dictionary of anatomy templates. It can raise + + """ + + def __init__(self, in_data, key=None, parent=None, strict=None): + super(TemplatesResultDict, self).__init__() + for _key, _value in in_data.items(): + if isinstance(_value, TemplatesResultDict): + _value.parent = self + elif isinstance(_value, dict): + _value = self.__class__(_value, _key, self) + self[_key] = _value + + if strict is None and parent is None: + strict = True + + self.key = key + self.parent = parent + self._is_strict = strict + + def __getitem__(self, key): + if key not in self.keys(): + hier = self.get_hierarchy() + hier.append(key) + raise TemplateMissingKey(hier) + + value = super(TemplatesResultDict, self).__getitem__(key) + if isinstance(value, self.__class__): + return value + + # Raise exception when expected solved templates and it is not. + if self.is_strict and hasattr(value, "validate"): + value.validate() + return value + + def get_is_strict(self): + return self._is_strict + + def set_is_strict(self, is_strict): + if is_strict is None and self.parent is None: + is_strict = True + self._is_strict = is_strict + for child in self.values(): + if isinstance(child, self.__class__): + child.set_is_strict(is_strict) + elif isinstance(child, AnatomyTemplateResult): + child.strict = is_strict + + strict = property(get_is_strict, set_is_strict) + is_strict = property(get_is_strict, set_is_strict) + + def get_hierarchy(self): + """Return dictionary keys one by one to root parent.""" + if self.key is None: + return [] + + if self.parent is None: + return [self.key] + + par_hier = list(self.parent.get_hierarchy()) + par_hier.append(self.key) + return par_hier + + @property + def missing_keys(self): + """Return missing keys of all children templates.""" + missing_keys = set() + for value in self.values(): + missing_keys |= value.missing_keys + return missing_keys + + @property + def invalid_types(self): + """Return invalid types of all children templates.""" + invalid_types = {} + for value in self.values(): + invalid_types = _merge_dict(invalid_types, value.invalid_types) + return invalid_types + + @property + def used_values(self): + """Return used values for all children templates.""" + used_values = {} + for value in self.values(): + used_values = _merge_dict(used_values, value.used_values) + return used_values + + def get_solved(self): + """Get only solved key from templates.""" + result = {} + for key, value in self.items(): + if isinstance(value, self.__class__): + value = value.get_solved() + if not value: + continue + result[key] = value + + elif ( + not hasattr(value, "solved") or + value.solved + ): + result[key] = value + return self.__class__(result, key=self.key, parent=self.parent) + + +class TemplateItem: + """Template item under template category. + + This item data usually contains 'file' and 'directory' by anatomy + definition, enhanced by common data ('frame_padding', + 'version_padding'). It adds 'path' key which is combination of + 'file' and 'directory' values. + + Args: + anatomy_templates (AnatomyTemplates): Anatomy templates object. + template_data (dict[str, Any]): Templates data. + + """ + def __init__(self, anatomy_templates, template_data): + template_data = copy.deepcopy(template_data) + + # Backwards compatibility for 'folder' + # TODO remove when deprecation not needed anymore + if ( + "folder" not in template_data + and "directory" in template_data + ): + template_data["folder"] = template_data["directory"] + + # Add 'path' key + if ( + "path" not in template_data + and "file" in template_data + and "directory" in template_data + ): + template_data["path"] = "/".join( + (template_data["directory"], template_data["file"]) + ) + + for key, value in template_data.items(): + if isinstance(value, str): + value = AnatomyStringTemplate(anatomy_templates, value) + template_data[key] = value + + self._template_data = template_data + self._anatomy_templates = anatomy_templates + + def __getitem__(self, key): + return self._template_data[key] + + def get(self, key, default=None): + return self._template_data.get(key, default) + + def format(self, data, strict=True): + output = {} + for key, value in self._template_data.items(): + if isinstance(value, AnatomyStringTemplate): + value = value.format(data) + output[key] = value + return TemplatesResultDict(output, strict=strict) + + +class TemplateCategory: + """Template category. + + Template category groups template items for specific usage. Categories + available at the moment are 'work', 'publish', 'hero', 'delivery', + 'staging' and 'others'. + + Args: + anatomy_templates (AnatomyTemplates): Anatomy templates object. + category_name (str): Category name. + category_data (dict[str, Any]): Category data. + + """ + def __init__(self, anatomy_templates, category_name, category_data): + for key, value in category_data.items(): + if isinstance(value, dict): + value = TemplateItem(anatomy_templates, value) + elif isinstance(value, str): + value = AnatomyStringTemplate(anatomy_templates, value) + category_data[key] = value + self._name = category_name + self._name_prefix = "{}_".format(category_name) + self._category_data = category_data + + def __getitem__(self, key): + new_key = self._convert_getter_key(key) + return self._category_data[new_key] + + def get(self, key, default=None): + new_key = self._convert_getter_key(key) + return self._category_data.get(new_key, default) + + @property + def name(self): + """Category name. + + Returns: + str: Category name. + + """ + return self._name + + def format(self, data, strict=True): + output = {} + for key, value in self._category_data.items(): + if isinstance(value, TemplateItem): + value = value.format(data, strict) + elif isinstance(value, AnatomyStringTemplate): + value = value.format(data) + + if isinstance(value, TemplatesResultDict): + value.key = key + output[key] = value + return TemplatesResultDict(output, key=self.name, strict=strict) + + def _convert_getter_key(self, key): + """Convert key for backwards compatibility. + + OpenPype compatible settings did contain template keys prefixed by + category name e.g. 'publish_render' which should be just 'render'. + + This method keeps the backwards compatibility but only if the key + starts with the category name prefix and the key is available in + roots. + + Args: + key (str): Key to be converted. + + Returns: + str: Converted string. + + """ + if key in self._category_data: + return key + + # Use default when the key is the category name + if key == self._name: + return "default" + + # Remove prefix if is key prefixed + if key.startswith(self._name_prefix): + new_key = key[len(self._name_prefix):] + if new_key in self._category_data: + return new_key + return key + + +class AnatomyTemplates: + inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") + inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") + + def __init__(self, anatomy): + self._anatomy = anatomy + + self._loaded_project = None + self._raw_templates = None + self._templates = None + self._objected_templates = None + + def __getitem__(self, key): + self._validate_discovery() + return self._objected_templates[key] + + def get(self, key, default=None): + self._validate_discovery() + return self._objected_templates.get(key, default) + + def keys(self): + return self._objected_templates.keys() + + def reset(self): + self._raw_templates = None + self._templates = None + self._objected_templates = None + + @property + def anatomy(self): + """Anatomy instance. + + Returns: + Anatomy: Anatomy instance. + + """ + return self._anatomy + + @property + def project_name(self): + """Project name. + + Returns: + Union[str, None]: Project name if set, otherwise None. + + """ + return self._anatomy.project_name + + @property + def roots(self): + """Anatomy roots object. + + Returns: + RootItem: Anatomy roots data. + + """ + return self._anatomy.roots + + @property + def templates(self): + """Templates data. + + Templates data with replaced common data. + + Returns: + dict[str, Any]: Templates data. + + """ + self._validate_discovery() + return self._templates + + @property + def frame_padding(self): + """Default frame padding. + + Returns: + int: Frame padding used by default in templates. + + """ + self._validate_discovery() + return self["frame_padding"] + + @property + def version_padding(self): + """Default version padding. + + Returns: + int: Version padding used by default in templates. + + """ + self._validate_discovery() + return self["version_padding"] + + @classmethod + def get_rootless_path_from_result(cls, result): + """Calculate rootless path from formatting result. + + Args: + result (TemplateResult): Result of StringTemplate formatting. + + Returns: + str: Rootless path if result contains one of anatomy roots. + """ + + used_values = result.used_values + missing_keys = result.missing_keys + template = result.template + invalid_types = result.invalid_types + if ( + "root" not in used_values + or "root" in missing_keys + or "{root" not in template + ): + return + + for invalid_type in invalid_types: + if "root" in invalid_type: + return + + root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]}) + if not root_keys: + return + + output = str(result) + for used_root_keys in root_keys: + if not used_root_keys: + continue + + used_value = used_values + root_key = None + for key in used_root_keys: + used_value = used_value[key] + if root_key is None: + root_key = key + else: + root_key += "[{}]".format(key) + + root_key = "{" + root_key + "}" + output = output.replace(str(used_value), root_key) + + return output + + def format(self, data, strict=True): + """Fill all templates based on entered data. + + Args: + data (dict[str, Any]): Fill data used for template formatting. + strict (Optional[bool]): Raise exception is accessed value is + not fully filled. + + Returns: + TemplatesResultDict: Output `TemplateResult` have `strict` + attribute set to False so accessing unfilled keys in templates + won't raise any exceptions. + + """ + self._validate_discovery() + copy_data = copy.deepcopy(data) + roots = self._anatomy.roots + if roots: + copy_data["root"] = roots + + return self._solve_dict(copy_data, strict) + + def format_all(self, in_data): + """Fill all templates based on entered data. + + Deprecated: + Use `format` method with `strict=False` instead. + + Args: + in_data (dict): Containing keys to be filled into template. + + Returns: + TemplatesResultDict: Output `TemplateResult` have `strict` + attribute set to False so accessing unfilled keys in templates + won't raise any exceptions. + + """ + return self.format(in_data, strict=False) + + def get_template_item( + self, category_name, template_name, subkey=None, default=_PLACEHOLDER + ): + """Get template item from category. + + Args: + category_name (str): Category name. + template_name (str): Template name. + subkey (Optional[str]): Subkey name. + default (Any): Default value if template is not found. + + Returns: + Any: Template item or subkey value. + + Raises: + KeyError: When any passed key is not available. Raise of error + does not happen if 'default' is filled. + + """ + self._validate_discovery() + category = self.get(category_name) + if category is None: + if default is not _PLACEHOLDER: + return default + raise KeyError("Category '{}' not found.".format(category_name)) + + template_item = category.get(template_name) + if template_item is None: + if default is not _PLACEHOLDER: + return default + raise KeyError( + "Template '{}' not found in category '{}'.".format( + template_name, category_name + ) + ) + + if subkey is None: + return template_item + + item = template_item.get(subkey) + if item is not None: + return item + + if default is not _PLACEHOLDER: + return default + raise KeyError( + "Subkey '{}' not found in '{}/{}'.".format( + subkey, category_name, template_name + ) + ) + + def _solve_dict(self, data, strict): + """ Solves templates with entered data. + + Args: + data (dict): Containing keys to be filled into template. + + Returns: + dict: With `TemplateResult` in values containing filled or + partially filled templates. + + """ + output = {} + for key, value in self._objected_templates.items(): + if isinstance(value, TemplateCategory): + value = value.format(data, strict) + elif isinstance(value, AnatomyStringTemplate): + value = value.format(data) + output[key] = value + return TemplatesResultDict(output, strict=strict) + + def _validate_discovery(self): + """Validate if templates are discovered and loaded for anatomy project. + + When project changes the cached data are reset and discovered again. + """ + if self.project_name != self._loaded_project: + self.reset() + + if self._templates is None: + self._discover() + self._loaded_project = self.project_name + + def _create_objected_templates(self, templates): + """Create objected templates from templates data. + + Args: + templates (dict[str, Any]): Templates data from project entity. + + Returns: + dict[str, Any]: Values are cnmverted to template objects. + + """ + objected_templates = {} + for category_name, category_value in copy.deepcopy(templates).items(): + if isinstance(category_value, dict): + category_value = TemplateCategory( + self, category_name, category_value + ) + elif isinstance(category_value, str): + category_value = AnatomyStringTemplate(self, category_value) + objected_templates[category_name] = category_value + return objected_templates + + def _discover(self): + """Load and cache templates from project entity.""" + if self.project_name is None: + raise ProjectNotSet("Anatomy project is not set.") + + templates = self.anatomy["templates"] + self._raw_templates = copy.deepcopy(templates) + + templates = copy.deepcopy(templates) + # Make sure all the keys are available + for key in ( + "publish", + "hero", + "work", + "delivery", + "staging", + "others", + ): + templates.setdefault(key, {}) + + solved_templates = self._solve_template_inner_links(templates) + self._templates = solved_templates + self._objected_templates = self._create_objected_templates( + solved_templates + ) + + @classmethod + def _replace_inner_keys(cls, matches, value, key_values, key): + """Replacement of inner keys in template values.""" + for match in matches: + anatomy_sub_keys = ( + cls.inner_key_name_pattern.findall(match) + ) + if key in anatomy_sub_keys: + raise ValueError(( + "Unsolvable recursion in inner keys, " + "key: \"{}\" is in his own value." + " Can't determine source, please check Anatomy templates." + ).format(key)) + + for anatomy_sub_key in anatomy_sub_keys: + replace_value = key_values.get(anatomy_sub_key) + if replace_value is None: + raise KeyError(( + "Anatomy templates can't be filled." + " Anatomy key `{0}` has" + " invalid inner key `{1}`." + ).format(key, anatomy_sub_key)) + + if not ( + isinstance(replace_value, numbers.Number) + or isinstance(replace_value, str) + ): + raise ValueError(( + "Anatomy templates can't be filled." + " Anatomy key `{0}` has" + " invalid inner key `{1}`" + " with value `{2}`." + ).format(key, anatomy_sub_key, str(replace_value))) + + value = value.replace(match, str(replace_value)) + + return value + + @classmethod + def _prepare_inner_keys(cls, key_values): + """Check values of inner keys. + + Check if inner key exist in template group and has valid value. + It is also required to avoid infinite loop with unsolvable recursion + when first inner key's value refers to second inner key's value where + first is used. + """ + keys_to_solve = set(key_values.keys()) + while True: + found = False + for key in tuple(keys_to_solve): + value = key_values[key] + + if isinstance(value, str): + matches = cls.inner_key_pattern.findall(value) + if not matches: + keys_to_solve.remove(key) + continue + + found = True + key_values[key] = cls._replace_inner_keys( + matches, value, key_values, key + ) + continue + + elif not isinstance(value, dict): + keys_to_solve.remove(key) + continue + + subdict_found = False + for _key, _value in tuple(value.items()): + matches = cls.inner_key_pattern.findall(_value) + if not matches: + continue + + subdict_found = True + found = True + key_values[key][_key] = cls._replace_inner_keys( + matches, _value, key_values, + "{}.{}".format(key, _key) + ) + + if not subdict_found: + keys_to_solve.remove(key) + + if not found: + break + + return key_values + + @classmethod + def _solve_template_inner_links(cls, templates): + """Solve templates inner keys identified by "{@*}". + + Process is split into 2 parts. + First is collecting all global keys (keys in top hierarchy where value + is not dictionary). All global keys are set for all group keys (keys + in top hierarchy where value is dictionary). Value of a key is not + overridden in group if already contain value for the key. + + In second part all keys with "at" symbol in value are replaced with + value of the key afterward "at" symbol from the group. + + Args: + templates (dict): Raw templates data. + + Example: + templates:: + key_1: "value_1", + key_2: "{@key_1}/{filling_key}" + + group_1: + key_3: "value_3/{@key_2}" + + group_2: + key_2": "value_2" + key_4": "value_4/{@key_2}" + + output:: + key_1: "value_1" + key_2: "value_1/{filling_key}" + + group_1: { + key_1: "value_1" + key_2: "value_1/{filling_key}" + key_3: "value_3/value_1/{filling_key}" + + group_2: { + key_1: "value_1" + key_2: "value_2" + key_4: "value_3/value_2" + + Returns: + dict[str, Any]: Solved templates data. + + """ + default_key_values = templates.pop("common", {}) + output = {} + for category_name, category_value in templates.items(): + new_category_value = {} + for key, value in category_value.items(): + key_values = copy.deepcopy(default_key_values) + key_values.update(value) + new_category_value[key] = cls._prepare_inner_keys(key_values) + output[category_name] = new_category_value + + default_keys_by_subkeys = cls._prepare_inner_keys(default_key_values) + for key, value in default_keys_by_subkeys.items(): + output[key] = value + + return output + + @classmethod + def _dict_to_subkeys_list(cls, subdict): + """Convert dictionary to list of subkeys. + + Example:: + + _dict_to_subkeys_list({ + "root": { + "work": "path/to/work", + "publish": "path/to/publish" + } + }) + [ + ["root", "work"], + ["root", "publish"] + ] + + + Args: + dict[str, Any]: Dictionary to be converted. + + Returns: + list[list[str]]: List of subkeys. + + """ + output = [] + subkey_queue = collections.deque() + subkey_queue.append((subdict, [])) + while subkey_queue: + queue_item = subkey_queue.popleft() + data, pre_keys = queue_item + for key, value in data.items(): + result = list(pre_keys) + result.append(key) + if isinstance(value, dict): + subkey_queue.append((value, result)) + else: + output.append(result) + return output diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 50c384bf88..84a17be8f2 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -545,7 +545,7 @@ def get_workdir_from_session(session=None, template_key=None): ) anatomy = Anatomy(project_name) - template_obj = anatomy.templates_obj[template_key]["folder"] + template_obj = anatomy.get_template_item("work", template_key, "directory") path = template_obj.format_strict(template_data) if path: path = os.path.normpath(path) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 308762fae6..8c6a7f1bb6 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1953,11 +1953,11 @@ class CreateContext: """Trigger create of plugins with standartized arguments. Arguments 'folder_entity' and 'task_name' use current context as - default values. If only 'task_name' is provided it will be overriden - by task name from current context. If 'task_name' is not provided - when 'folder_entity' is, it is considered that task name is not - specified, which can lead to error if product name template requires - task name. + default values. If only 'task_entity' is provided it will be + overridden by task name from current context. If 'task_name' is not + provided when 'folder_entity' is, it is considered that task name is + not specified, which can lead to error if product name template + requires task name. Args: creator_identifier (str): Identifier of creator plugin. @@ -1986,6 +1986,8 @@ class CreateContext: raise CreatorError( "Folder '{}' was not found".format(folder_path) ) + + task_name = None if task_entity is None: task_name = self.get_current_task_name() task_entity = ayon_api.get_task_by_name( diff --git a/client/ayon_core/pipeline/create/legacy_create.py b/client/ayon_core/pipeline/create/legacy_create.py index a8a39b41e3..ab939343c9 100644 --- a/client/ayon_core/pipeline/create/legacy_create.py +++ b/client/ayon_core/pipeline/create/legacy_create.py @@ -44,7 +44,7 @@ class LegacyCreator(object): @classmethod def apply_settings(cls, project_settings): - """Apply OpenPype settings to a plugin class.""" + """Apply AYON settings to a plugin class.""" host_name = os.environ.get("AYON_HOST_NAME") plugin_type = "create" diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index d2b78422e3..029775e1db 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -77,8 +77,10 @@ def check_destination_path( """ anatomy_data.update(datetime_data) - anatomy_filled = anatomy.format_all(anatomy_data) - dest_path = anatomy_filled["delivery"][template_name] + path_template = anatomy.get_template_item( + "delivery", template_name, "path" + ) + dest_path = path_template.format(anatomy_data) report_items = collections.defaultdict(list) if not dest_path.solved: @@ -150,7 +152,9 @@ def deliver_single_file( if format_dict: anatomy_data = copy.deepcopy(anatomy_data) anatomy_data["root"] = format_dict["root"] - template_obj = anatomy.templates_obj["delivery"][template_name] + template_obj = anatomy.get_template_item( + "delivery", template_name, "path" + ) delivery_path = template_obj.format_strict(anatomy_data) # Backwards compatibility when extension contained `.` @@ -220,8 +224,9 @@ def deliver_sequence( report_items["Source file was not found"].append(msg) return report_items, 0 - delivery_templates = anatomy.templates.get("delivery") or {} - delivery_template = delivery_templates.get(template_name) + delivery_template = anatomy.get_template_item( + "delivery", template_name, "path", default=None + ) if delivery_template is None: msg = ( "Delivery template \"{}\" in anatomy of project \"{}\"" @@ -232,7 +237,7 @@ def deliver_sequence( # Check if 'frame' key is available in template which is required # for sequence delivery - if "{frame" not in delivery_template: + if "{frame" not in delivery_template.template: msg = ( "Delivery template \"{}\" in anatomy of project \"{}\"" "does not contain '{{frame}}' key to fill. Delivery of sequence" @@ -277,8 +282,7 @@ def deliver_sequence( anatomy_data["frame"] = frame_indicator if format_dict: anatomy_data["root"] = format_dict["root"] - template_obj = anatomy.templates_obj["delivery"][template_name] - delivery_path = template_obj.format_strict(anatomy_data) + delivery_path = delivery_template.format_strict(anatomy_data) delivery_path = os.path.normpath(delivery_path.replace("\\", "/")) delivery_folder = os.path.dirname(delivery_path) diff --git a/client/ayon_core/pipeline/farm/tools.py b/client/ayon_core/pipeline/farm/tools.py index 8ab3b87ff6..0b647340f3 100644 --- a/client/ayon_core/pipeline/farm/tools.py +++ b/client/ayon_core/pipeline/farm/tools.py @@ -54,7 +54,7 @@ def from_published_scene(instance, replace_in_path=True): template_data["comment"] = None anatomy = instance.context.data['anatomy'] - template_obj = anatomy.templates_obj["publish"]["path"] + template_obj = anatomy.get_template_item("publish", "default", "path") template_filled = template_obj.format_strict(template_data) file_path = os.path.normpath(template_filled) diff --git a/client/ayon_core/pipeline/publish/README.md b/client/ayon_core/pipeline/publish/README.md index 2a0f45d093..ee2124dfd3 100644 --- a/client/ayon_core/pipeline/publish/README.md +++ b/client/ayon_core/pipeline/publish/README.md @@ -1,8 +1,8 @@ # Publish -OpenPype is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. OpenPype's (new) publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception. +AYON is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. OpenPype's (new) publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception. ## Exceptions -OpenPype define few specific exceptions that should be used in publish plugins. +AYON define few specific exceptions that should be used in publish plugins. ### Validation exception Validation plugins should raise `PublishValidationError` to show to an artist what's wrong and give him actions to fix it. The exception says that error happened in plugin can be fixed by artist himself (with or without action on plugin). Any other errors will stop publishing immediately. Exception `PublishValidationError` raised after validation order has same effect as any other exception. @@ -35,4 +35,4 @@ class MyExtendedPlugin( ### Extensions Currently only extension is ability to define attributes for instances during creation. Method `get_attribute_defs` returns attribute definitions for families defined in plugin's `families` attribute if it's instance plugin or for whole context if it's context plugin. To convert existing values (or to remove legacy values) can be implemented `convert_attribute_values`. Values of publish attributes from created instance are never removed automatically so implementing of this method is best way to remove legacy data or convert them to new data structure. -Possible attribute definitions can be found in `openpype/pipeline/lib/attribute_definitions.py`. +Possible attribute definitions can be found in `ayon_core/lib/attribute_definitions.py`. diff --git a/client/ayon_core/pipeline/publish/constants.py b/client/ayon_core/pipeline/publish/constants.py index 92e3fb089f..38f5ffef3f 100644 --- a/client/ayon_core/pipeline/publish/constants.py +++ b/client/ayon_core/pipeline/publish/constants.py @@ -6,6 +6,6 @@ ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 -DEFAULT_PUBLISH_TEMPLATE = "publish" -DEFAULT_HERO_PUBLISH_TEMPLATE = "hero" -TRANSIENT_DIR_TEMPLATE = "transient" +DEFAULT_PUBLISH_TEMPLATE = "default" +DEFAULT_HERO_PUBLISH_TEMPLATE = "default" +TRANSIENT_DIR_TEMPLATE = "default" diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index b4ed69b5d7..8d3644637b 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -742,29 +742,18 @@ def get_custom_staging_dir_info( anatomy = Anatomy(project_name) template_name = profile["template_name"] or TRANSIENT_DIR_TEMPLATE - _validate_transient_template(project_name, template_name, anatomy) - custom_staging_dir = anatomy.templates[template_name]["folder"] + custom_staging_dir = anatomy.get_template_item( + "staging", template_name, "directory", default=None + ) + if custom_staging_dir is None: + raise ValueError(( + "Anatomy of project \"{}\" does not have set" + " \"{}\" template key!" + ).format(project_name, template_name)) is_persistent = profile["custom_staging_dir_persistent"] - return custom_staging_dir, is_persistent - - -def _validate_transient_template(project_name, template_name, anatomy): - """Check that transient template is correctly configured. - - Raises: - ValueError - if misconfigured template - """ - if template_name not in anatomy.templates: - raise ValueError(("Anatomy of project \"{}\" does not have set" - " \"{}\" template key!" - ).format(project_name, template_name)) - - if "folder" not in anatomy.templates[template_name]: - raise ValueError(("There is not set \"folder\" template in \"{}\" anatomy" # noqa - " for project \"{}\"." - ).format(template_name, project_name)) + return custom_staging_dir.template, is_persistent def get_published_workfile_instance(context): @@ -815,9 +804,9 @@ def replace_with_published_scene_path(instance, replace_in_path=True): template_data["ext"] = rep.get("ext") template_data["comment"] = None - anatomy = instance.context.data['anatomy'] - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] + anatomy = instance.context.data["anatomy"] + template = anatomy.get_template_item("publish", "default", "path") + template_filled = template.format_strict(template_data) file_path = os.path.normpath(template_filled) log.info("Using published scene for render {}".format(file_path)) diff --git a/client/ayon_core/pipeline/schema/__init__.py b/client/ayon_core/pipeline/schema/__init__.py index 3abc576f89..67cf120b59 100644 --- a/client/ayon_core/pipeline/schema/__init__.py +++ b/client/ayon_core/pipeline/schema/__init__.py @@ -44,11 +44,6 @@ def validate(data, schema=None): _precache() root, schema = data["schema"].rsplit(":", 1) - # assert root in ( - # "mindbender-core", # Backwards compatiblity - # "avalon-core", - # "pype" - # ) if isinstance(schema, six.string_types): schema = _cache[schema + ".json"] diff --git a/client/ayon_core/pipeline/usdlib.py b/client/ayon_core/pipeline/usdlib.py index dedcee6f99..1c7943441e 100644 --- a/client/ayon_core/pipeline/usdlib.py +++ b/client/ayon_core/pipeline/usdlib.py @@ -341,7 +341,9 @@ def get_usd_master_path(folder_entity, product_name, representation): "version": 0, # stub version zero }) - template_obj = anatomy.templates_obj["publish"]["path"] + template_obj = anatomy.get_template_item( + "publish", "default", "path" + ) path = template_obj.format_strict(template_data) # Remove the version folder diff --git a/client/ayon_core/pipeline/workfile/path_resolving.py b/client/ayon_core/pipeline/workfile/path_resolving.py index 5d36f432ad..47d6f4ddfa 100644 --- a/client/ayon_core/pipeline/workfile/path_resolving.py +++ b/client/ayon_core/pipeline/workfile/path_resolving.py @@ -135,7 +135,9 @@ def get_workdir_with_workdir_data( project_settings ) - template_obj = anatomy.templates_obj[template_key]["folder"] + template_obj = anatomy.get_template_item( + "work", template_key, "directory" + ) # Output is TemplateResult object which contain useful data output = template_obj.format_strict(workdir_data) if output: @@ -309,11 +311,12 @@ def get_last_workfile( Returns file with version 1 if there is not workfile yet. Args: - workdir(str): Path to dir where workfiles are stored. - file_template(str): Template of file name. - fill_data(Dict[str, Any]): Data for filling template. - extensions(Iterable[str]): All allowed file extensions of workfile. - full_path(bool): Full path to file is returned if set to True. + workdir (str): Path to dir where workfiles are stored. + file_template (str): Template of file name. + fill_data (Dict[str, Any]): Data for filling template. + extensions (Iterable[str]): All allowed file extensions of workfile. + full_path (Optional[bool]): Full path to file is returned if + set to True. Returns: str: Last or first workfile as filename of full path to filename. @@ -334,7 +337,7 @@ def get_last_workfile( data.pop("comment", None) if not data.get("ext"): data["ext"] = extensions[0] - data["ext"] = data["ext"].replace('.', '') + data["ext"] = data["ext"].lstrip(".") filename = StringTemplate.format_strict_template(file_template, data) if full_path: diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 124952b2c0..1d7b5ed5a7 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1726,7 +1726,7 @@ class PlaceholderCreateMixin(object): items=creator_items, tooltip=( "Creator" - "\nDefines what OpenPype creator will be used to" + "\nDefines what AYON creator will be used to" " create publishable instance." "\nUseable creator depends on current host's creator list." "\nField is case sensitive." diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index c221752f11..69375a7859 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -70,7 +70,9 @@ class OpenTaskPath(LauncherAction): data = get_template_data(project_entity, folder_entity, task_entity) anatomy = Anatomy(project_name) - workdir = anatomy.templates_obj["work"]["folder"].format(data) + workdir = anatomy.get_template_item( + "work", "default", "folder" + ).format(data) # Remove any potential un-formatted parts of the path valid_workdir = self._find_first_filled_path(workdir) @@ -85,7 +87,9 @@ class OpenTaskPath(LauncherAction): return valid_workdir data.pop("task", None) - workdir = anatomy.templates_obj["work"]["folder"].format(data) + workdir = anatomy.get_template_item( + "work", "default", "folder" + ).format(data) valid_workdir = self._find_first_filled_path(workdir) if valid_workdir: # Normalize diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index cfa245d0d2..f8184c8567 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -282,7 +282,11 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): """Adds list of delivery templates from Anatomy to dropdown.""" templates = {} for template_name, value in anatomy.templates["delivery"].items(): - if not isinstance(value, str) or not value.startswith('{root'): + path_template = value["path"] + if ( + not isinstance(path_template, str) + or not path_template.startswith('{root') + ): continue templates[template_name] = value diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 3f47e6e3bf..37a5e87a7a 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -43,8 +43,10 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): template_name = self.get_template_name(instance) anatomy = instance.context.data["anatomy"] - publish_template_category = anatomy.templates[template_name] - template = os.path.normpath(publish_template_category["path"]) + publish_path_template = anatomy.get_template_item( + "publish", template_name, "path" + ).template + template = os.path.normpath(publish_path_template) self.log.debug( ">> template: {}".format(template)) diff --git a/client/ayon_core/plugins/publish/collect_resources_path.py b/client/ayon_core/plugins/publish/collect_resources_path.py index 6a871124f1..959523918e 100644 --- a/client/ayon_core/plugins/publish/collect_resources_path.py +++ b/client/ayon_core/plugins/publish/collect_resources_path.py @@ -79,23 +79,12 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "representation": "TEMP" }) - publish_templates = anatomy.templates_obj["publish"] - if "folder" in publish_templates: - publish_folder = publish_templates["folder"].format_strict( - template_data - ) - else: - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(anatomy.project_name)) - - file_path = publish_templates["path"].format_strict(template_data) - publish_folder = os.path.dirname(file_path) - - publish_folder = os.path.normpath(publish_folder) + publish_templates = anatomy.get_template_item( + "publish", "default", "directory" + ) + publish_folder = os.path.normpath( + publish_templates.format_strict(template_data) + ) resources_folder = os.path.join(publish_folder, "resources") instance.data["publishDir"] = publish_folder diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index b7839338ae..ce34f2e88b 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -665,8 +665,9 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.debug("Anatomy template name: {}".format(template_name)) anatomy = instance.context.data["anatomy"] - publish_template_category = anatomy.templates[template_name] - template = os.path.normpath(publish_template_category["path"]) + publish_template = anatomy.get_template_item("publish", template_name) + path_template_obj = publish_template["path"] + template = path_template_obj.template.replace("\\", "/") is_udim = bool(repre.get("udim")) @@ -698,7 +699,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # - template_data (Dict[str, Any]): source data used to fill template # - to add required data to 'repre_context' not used for # formatting - path_template_obj = anatomy.templates_obj[template_name]["path"] # Treat template with 'orignalBasename' in special way if "{originalBasename}" in template: @@ -753,9 +753,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if not is_udim: # Change padding for frames if template has defined higher # padding. - template_padding = int( - publish_template_category["frame_padding"] - ) + template_padding = anatomy.templates_obj.frame_padding if template_padding > destination_padding: destination_padding = template_padding @@ -841,7 +839,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # todo: Are we sure the assumption each representation # ends up in the same folder is valid? if not instance.data.get("publishDir"): - template_obj = anatomy.templates_obj[template_name]["folder"] + template_obj = publish_template["directory"] template_filled = template_obj.format_strict(template_data) instance.data["publishDir"] = template_filled diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 6d690ba94b..7969457697 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -13,7 +13,10 @@ from ayon_api.operations import ( from ayon_api.utils import create_entity_id from ayon_core.lib import create_hard_link, source_hash -from ayon_core.pipeline.publish import get_publish_template_name +from ayon_core.pipeline.publish import ( + get_publish_template_name, + OptionalPyblishPluginMixin, +) def prepare_changes(old_entity, new_entity): @@ -46,7 +49,9 @@ def prepare_changes(old_entity, new_entity): return changes -class IntegrateHeroVersion(pyblish.api.InstancePlugin): +class IntegrateHeroVersion( + OptionalPyblishPluginMixin, pyblish.api.InstancePlugin +): label = "Integrate Hero Version" # Must happen after IntegrateNew order = pyblish.api.IntegratorOrder + 0.1 @@ -84,8 +89,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): # permissions error on files (files were used or user didn't have perms) # *but all other plugins must be sucessfully completed - _default_template_name = "hero" - def process(self, instance): self.log.debug( "--- Integration of Hero version for product `{}` begins.".format( @@ -103,22 +106,17 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): project_name = anatomy.project_name template_key = self._get_template_key(project_name, instance) + hero_template = anatomy.get_template_item( + "hero", template_key, "path", default=None + ) - if template_key not in anatomy.templates: + if hero_template is None: self.log.warning(( "!!! Anatomy of project \"{}\" does not have set" " \"{}\" template key!" ).format(project_name, template_key)) return - if "path" not in anatomy.templates[template_key]: - self.log.warning(( - "!!! There is not set \"path\" template in \"{}\" anatomy" - " for project \"{}\"." - ).format(template_key, project_name)) - return - - hero_template = anatomy.templates[template_key]["path"] self.log.debug("`hero` template check was successful. `{}`".format( hero_template )) @@ -327,7 +325,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): try: src_to_dst_file_paths = [] repre_integrate_data = [] - path_template_obj = anatomy.templates_obj[template_key]["path"] + path_template_obj = anatomy.get_template_item( + "hero", template_key, "path" + ) for repre_info in published_repres.values(): # Skip if new repre does not have published repre files @@ -340,7 +340,9 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_data.pop("version", None) # Get filled path to repre context - template_filled = path_template_obj.format_strict(anatomy_data) + template_filled = path_template_obj.format_strict( + anatomy_data + ) repre_context = template_filled.used_values for key in self.db_representation_context_keys: value = anatomy_data.get(key) @@ -354,7 +356,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): repre_entity["context"] = repre_context repre_entity["attrib"] = { "path": str(template_filled), - "template": hero_template + "template": hero_template.template } dst_paths = [] @@ -383,9 +385,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): anatomy_data ) head, tail = _template_filled.split(frame_splitter) - padding = int( - anatomy.templates[template_key]["frame_padding"] - ) + padding = anatomy.templates_obj.frame_padding dst_col = clique.Collection( head=head, padding=padding, tail=tail @@ -545,29 +545,12 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): "originalBasename": instance.data.get("originalBasename") }) - if "folder" in anatomy.templates[template_key]: - template_obj = anatomy.templates_obj[template_key]["folder"] - publish_folder = template_obj.format_strict(template_data) - else: - # This is for cases of Deprecated anatomy without `folder` - # TODO remove when all clients have solved this issue - self.log.warning(( - "Deprecation warning: Anatomy does not have set `folder`" - " key underneath `publish` (in global of for project `{}`)." - ).format(anatomy.project_name)) - # solve deprecated situation when `folder` key is not underneath - # `publish` anatomy - template_data.update({ - "frame": "FRAME_TEMP", - "representation": "TEMP" - }) - template_obj = anatomy.templates_obj[template_key]["path"] - file_path = template_obj.format_strict(template_data) - - # Directory - publish_folder = os.path.dirname(file_path) - - publish_folder = os.path.normpath(publish_folder) + template_obj = anatomy.get_template_item( + "hero", template_key, "directory" + ) + publish_folder = os.path.normpath( + template_obj.format_strict(template_data) + ) self.log.debug("hero publish dir: \"{}\"".format(publish_folder)) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 8a29da2fe4..6e43050c05 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -973,12 +973,9 @@ class ProjectPushItemProcess: "version": version_entity["version"] }) - path_template = anatomy.templates[template_name]["path"].replace( - "\\", "/" - ) - file_template = StringTemplate( - anatomy.templates[template_name]["file"] - ) + publish_template = anatomy.get_template_item("publish", template_name) + path_template = publish_template["path"].template.replace("\\", "/") + file_template = publish_template["file"] self._log_info("Preparing files to transfer") processed_repre_items = self._prepare_file_transactions( anatomy, template_name, formatting_data, file_template @@ -1014,7 +1011,9 @@ class ProjectPushItemProcess: if repre_output_name is not None: repre_format_data["output"] = repre_output_name - template_obj = anatomy.templates_obj[template_name]["folder"] + template_obj = anatomy.get_template_item( + "publish", template_name, "directory" + ) folder_path = template_obj.format_strict(formatting_data) repre_context = folder_path.used_values folder_path_rootless = folder_path.rootless diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index df0dea7a3d..330b174218 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -68,13 +68,7 @@ class InventoryModel(TreeModel): } def outdated(self, item): - value = item.get("version") - if isinstance(value, HeroVersionType): - return False - - if item.get("version") == item.get("highest_version"): - return False - return True + return item.get("isOutdated", True) def data(self, index, role): if not index.isValid(): @@ -297,6 +291,21 @@ class InventoryModel(TreeModel): ) sites_info = self._controller.get_sites_information() + # Query the highest available version so the model can know + # whether current version is currently up-to-date. + highest_version_by_product_id = ayon_api.get_last_versions( + project_name, + product_ids={ + group["version"]["productId"] for group in grouped.values() + }, + fields={"productId", "version"} + ) + # Map value to `version` key + highest_version_by_product_id = { + product_id: version["version"] + for product_id, version in highest_version_by_product_id.items() + } + for repre_id, group_dict in sorted(grouped.items()): group_containers = group_dict["containers"] repre_entity = group_dict["representation"] @@ -306,12 +315,6 @@ class InventoryModel(TreeModel): product_type = product_entity["productType"] - # Store the highest available version so the model can know - # whether current version is currently up-to-date. - highest_version = ayon_api.get_last_version_by_product_id( - project_name, version_entity["productId"] - ) - # create the group header group_node = Item() group_node["Name"] = "{}_{}: ({})".format( @@ -320,8 +323,23 @@ class InventoryModel(TreeModel): repre_entity["name"] ) group_node["representation"] = repre_id - group_node["version"] = version_entity["version"] - group_node["highest_version"] = highest_version["version"] + + # Detect hero version type + version = version_entity["version"] + if version < 0: + version = HeroVersionType(version) + group_node["version"] = version + + # Check if the version is outdated. + # Hero versions are never considered to be outdated. + is_outdated = False + if not isinstance(version, HeroVersionType): + last_version = highest_version_by_product_id.get( + version_entity["productId"]) + if last_version is not None: + is_outdated = version_entity["version"] != last_version + group_node["isOutdated"] = is_outdated + group_node["productType"] = product_type or "" group_node["productTypeIcon"] = product_type_icon group_node["count"] = len(group_containers) @@ -490,27 +508,11 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): def _is_outdated(self, row, parent): """Return whether row is outdated. - A row is considered outdated if it has "version" and "highest_version" - data and in the internal data structure, and they are not of an - equal value. + A row is considered outdated if `isOutdated` data is true or not set. """ def outdated(node): - version = node.get("version", None) - highest = node.get("highest_version", None) - - # Always allow indices that have no version data at all - if version is None and highest is None: - return True - - # If either a version or highest is present but not the other - # consider the item invalid. - if not self._hierarchy_view: - # Skip this check if in hierarchy view, or the child item - # node will be hidden even it's actually outdated. - if version is None or highest is None: - return False - return version != highest + return node.get("isOutdated", True) index = self.sourceModel().index(row, self.filterKeyColumn(), parent) diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py index 823a4a6631..4977ad13c6 100644 --- a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py +++ b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py @@ -240,7 +240,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): return ayon_api.get_last_versions( project_name, product_ids, - fields={"id", "folderId", "version"} + fields={"id", "productId", "version"} ) def _on_show_timer(self): diff --git a/client/ayon_core/tools/texture_copy/app.py b/client/ayon_core/tools/texture_copy/app.py index b3484fbcd0..c288187aac 100644 --- a/client/ayon_core/tools/texture_copy/app.py +++ b/client/ayon_core/tools/texture_copy/app.py @@ -40,7 +40,9 @@ class TextureCopy: }, }) anatomy = Anatomy(project_name, project_entity=project_entity) - template_obj = anatomy.templates_obj["texture"]["path"] + template_obj = anatomy.get_template_item( + "publish", "texture", "path" + ) return template_obj.format_strict(template_data) def _get_version(self, path): diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index 8bcfe8b985..4b7ca5425e 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -123,6 +123,7 @@ def paint_image_with_color(image, color): def format_version(value): """Formats integer to displayable version name""" + value = int(value) # convert e.g. HeroVersionType to its version value label = "v{0:03d}".format(abs(value)) if value < 0: return "[{}]".format(label) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 35a49a908f..5f59b99b22 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -25,7 +25,14 @@ from ayon_core.tools.workfiles.abstract import ( class CommentMatcher(object): - """Use anatomy and work file data to parse comments from filenames""" + """Use anatomy and work file data to parse comments from filenames. + + Args: + extensions (set[str]): Set of extensions. + file_template (AnatomyStringTemplate): File template. + data (dict[str, Any]): Data to fill the template with. + + """ def __init__(self, extensions, file_template, data): self.fname_regex = None @@ -199,8 +206,24 @@ class WorkareaModel: def _get_last_workfile_version( self, workdir, file_template, fill_data, extensions ): + """ + + Todos: + Validate if logic of this function is correct. It does return + last version + 1 which might be wrong. + + Args: + workdir (str): Workdir path. + file_template (str): File template. + fill_data (dict[str, Any]): Fill data. + extensions (set[str]): Extensions. + + Returns: + int: Next workfile version. + + """ version = get_last_workfile_with_version( - workdir, str(file_template), fill_data, extensions + workdir, file_template, fill_data, extensions )[1] if version is None: @@ -225,8 +248,21 @@ class WorkareaModel: root, current_filename, ): + """Get comments from root directory. + + Args: + file_template (AnatomyStringTemplate): File template. + extensions (set[str]): Extensions. + fill_data (dict[str, Any]): Fill data. + root (str): Root directory. + current_filename (str): Current filename. + + Returns: + Tuple[list[str], Union[str, None]]: Comment hints and current + comment. + + """ current_comment = None - comment_hints = set() filenames = [] if root and os.path.exists(root): for filename in os.listdir(root): @@ -239,10 +275,11 @@ class WorkareaModel: filenames.append(filename) if not filenames: - return comment_hints, current_comment + return [], current_comment matcher = CommentMatcher(extensions, file_template, fill_data) + comment_hints = set() for filename in filenames: comment = matcher.parse_comment(filename) if comment: @@ -253,23 +290,24 @@ class WorkareaModel: return list(comment_hints), current_comment def _get_workdir(self, anatomy, template_key, fill_data): - template_info = anatomy.templates_obj[template_key] - directory_template = template_info["folder"] + directory_template = anatomy.get_template_item( + "work", template_key, "directory" + ) return directory_template.format_strict(fill_data).normalized() def get_workarea_save_as_data(self, folder_id, task_id): - folder = None - task = None + folder_entity = None + task_entity = None if folder_id: - folder = self._controller.get_folder_entity( + folder_entity = self._controller.get_folder_entity( self.project_name, folder_id ) - if task_id: - task = self._controller.get_task_entity( - self.project_name, task_id - ) + if folder_entity and task_id: + task_entity = self._controller.get_task_entity( + self.project_name, task_id + ) - if not folder or not task: + if not folder_entity or not task_entity: return { "template_key": None, "template_has_version": None, @@ -299,8 +337,13 @@ class WorkareaModel: workdir = self._get_workdir(anatomy, template_key, fill_data) - template_info = anatomy.templates_obj[template_key] - file_template = template_info["file"] + file_template = anatomy.get_template_item( + "work", template_key, "file" + ) + file_template_str = file_template.template + + template_has_version = "{version" in file_template_str + template_has_comment = "{comment" in file_template_str comment_hints, comment = self._get_comments_from_root( file_template, @@ -310,10 +353,8 @@ class WorkareaModel: current_filename, ) last_version = self._get_last_workfile_version( - workdir, file_template, fill_data, extensions) - str_file_template = str(file_template) - template_has_version = "{version" in str_file_template - template_has_comment = "{comment" in str_file_template + workdir, file_template_str, fill_data, extensions + ) return { "template_key": template_key, @@ -336,18 +377,34 @@ class WorkareaModel: version, comment, ): + """Fill workarea filepath based on context. + + Args: + folder_id (str): Folder id. + task_id (str): Task id. + extension (str): File extension. + use_last_version (bool): Use last version. + version (int): Version number. + comment (str): Comment. + + Returns: + WorkareaFilepathResult: Workarea filepath result. + + """ anatomy = self._controller.project_anatomy fill_data = self._prepare_fill_data(folder_id, task_id) + template_key = self._get_template_key(fill_data) workdir = self._get_workdir(anatomy, template_key, fill_data) - template_info = anatomy.templates_obj[template_key] - file_template = template_info["file"] + file_template = anatomy.get_template_item( + "work", template_key, "file" + ) if use_last_version: version = self._get_last_workfile_version( - workdir, file_template, fill_data, self._extensions + workdir, file_template.template, fill_data, self._extensions ) fill_data["version"] = version fill_data["ext"] = extension.lstrip(".") diff --git a/server/settings/tools.py b/server/settings/tools.py index b45f9b49d4..488d27e8f1 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -410,14 +410,14 @@ DEFAULT_TOOLS_VALUES = { { "task_types": [], "hosts": [], - "workfile_template": "work" + "workfile_template": "default" }, { "task_types": [], "hosts": [ "unreal" ], - "workfile_template": "work_unreal" + "workfile_template": "unreal" } ], "last_workfile_on_startup": [ @@ -457,7 +457,7 @@ DEFAULT_TOOLS_VALUES = { "hosts": [], "task_types": [], "task_names": [], - "template_name": "publish" + "template_name": "default" }, { "product_types": [ @@ -468,7 +468,7 @@ DEFAULT_TOOLS_VALUES = { "hosts": [], "task_types": [], "task_names": [], - "template_name": "publish_render" + "template_name": "render" }, { "product_types": [ @@ -479,7 +479,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "publish_simpleUnrealTexture" + "template_name": "simpleUnrealTexture" }, { "product_types": [ @@ -491,7 +491,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "publish_maya2unreal" + "template_name": "maya2unreal" }, { "product_types": [ @@ -502,7 +502,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "publish_online" + "template_name": "online" }, { "product_types": [ @@ -513,7 +513,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "publish_tycache" + "template_name": "tycache" } ], "hero_template_name_profiles": [ @@ -526,7 +526,7 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "task_names": [], - "template_name": "hero_simpleUnrealTextureHero" + "template_name": "simpleUnrealTextureHero" } ] } diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 79c489d080..c742fdc5bd 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -89,6 +89,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ValidatePluginModel, title="Validate Mesh No Negative Scale" ) + ValidateModelMeshUvMap1: ValidatePluginModel = SettingsField( + default_factory=ValidatePluginModel, + title="Validate Model Mesh Has UV map named map1" + ) ValidateTransformZero: ValidatePluginModel = SettingsField( default_factory=ValidatePluginModel, title="Validate Transform Zero" @@ -181,6 +185,11 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": False, "active": True }, + "ValidateModelMeshUvMap1": { + "enabled": False, + "optional": True, + "active": True + }, "ValidateTransformZero": { "enabled": False, "optional": True, diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index f1380eede2..9cb17e7976 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.7" +__version__ = "0.1.8" diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 3a6de2eb44..ef23b77d19 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -753,14 +753,6 @@ class PublishersModel(BaseSettingsModel): default_factory=BasicValidateModel, title="Validate Ass Relative Paths" ) - ValidateInstancerContent: BasicValidateModel = SettingsField( - default_factory=BasicValidateModel, - title="Validate Instancer Content" - ) - ValidateInstancerFrameRanges: BasicValidateModel = SettingsField( - default_factory=BasicValidateModel, - title="Validate Instancer Cache Frame Ranges" - ) ValidateNoDefaultCameras: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, title="Validate No Default Cameras" @@ -1300,16 +1292,6 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": False, "active": True }, - "ValidateInstancerContent": { - "enabled": True, - "optional": False, - "active": True - }, - "ValidateInstancerFrameRanges": { - "enabled": True, - "optional": False, - "active": True - }, "ValidateNoDefaultCameras": { "enabled": True, "optional": False, diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index 8202425a2d..a86a3ce0a1 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.9" +__version__ = "0.1.10"