diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 258458e2d4..d9b4d8089c 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,7 +43,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3f85525c26..917e6c884c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,7 +39,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]},"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: "3.0.0" @@ -81,7 +81,7 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}}' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["type: documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]},"feature":{"prefix":"**🆕 New features**", "labels":["type: feature"]},"breaking":{"prefix":"**💥 Breaking**", "labels":["breaking"]},"enhancements":{"prefix":"**🚀 Enhancements**", "labels":["type: enhancement"]},"bugs":{"prefix":"**🐛 Bug fixes**", "labels":["type: bug"]},"deprecated":{"prefix":"**⚠️ Deprecations**", "labels":["depreciated"]}, "refactor":{"prefix":"**🔀 Refactored code**", "labels":["refactor"]}}' issues: false issuesWoLabels: false sinceTag: ${{ steps.version.outputs.last_release }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 711517e6c6..fa479d8f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [3.9.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.9.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.8.2...HEAD) **Deprecated:** +- AssetCreator: Remove the tool [\#2845](https://github.com/pypeclub/OpenPype/pull/2845) - Houdini: Remove unused code [\#2779](https://github.com/pypeclub/OpenPype/pull/2779) ### 📖 Documentation @@ -16,21 +17,23 @@ **🚀 Enhancements** +- New: Validation exceptions [\#2841](https://github.com/pypeclub/OpenPype/pull/2841) - Ftrack: Can sync fps as string [\#2836](https://github.com/pypeclub/OpenPype/pull/2836) +- General: Custom function for find executable [\#2822](https://github.com/pypeclub/OpenPype/pull/2822) - General: Color dialog UI fixes [\#2817](https://github.com/pypeclub/OpenPype/pull/2817) +- global: letter box calculated on output as last process [\#2812](https://github.com/pypeclub/OpenPype/pull/2812) - Nuke: adding Reformat to baking mov plugin [\#2811](https://github.com/pypeclub/OpenPype/pull/2811) - Manager: Update all to latest button [\#2805](https://github.com/pypeclub/OpenPype/pull/2805) - General: Set context environments for non host applications [\#2803](https://github.com/pypeclub/OpenPype/pull/2803) -- Houdini: Remove duplicate ValidateOutputNode plug-in [\#2780](https://github.com/pypeclub/OpenPype/pull/2780) - Tray publisher: New Tray Publisher host \(beta\) [\#2778](https://github.com/pypeclub/OpenPype/pull/2778) -- Slack: Added regex for filtering on subset names [\#2775](https://github.com/pypeclub/OpenPype/pull/2775) -- Houdini: Implement Reset Frame Range [\#2770](https://github.com/pypeclub/OpenPype/pull/2770) - Flame: use Shot Name on segment for asset name [\#2751](https://github.com/pypeclub/OpenPype/pull/2751) -- Houdini: Move Houdini Save Current File to beginning of ExtractorOrder [\#2747](https://github.com/pypeclub/OpenPype/pull/2747) -- RoyalRender: Minor enhancements [\#2700](https://github.com/pypeclub/OpenPype/pull/2700) **🐛 Bug fixes** +- WebPublisher: Fix username stored in DB [\#2852](https://github.com/pypeclub/OpenPype/pull/2852) +- WebPublisher: Fix wrong number of frames for video file [\#2851](https://github.com/pypeclub/OpenPype/pull/2851) +- Nuke: fix multiple baking profile farm publishing [\#2842](https://github.com/pypeclub/OpenPype/pull/2842) +- Blender: Fixed parameters for FBX export of the camera [\#2840](https://github.com/pypeclub/OpenPype/pull/2840) - Maya: Stop creation of reviews for Cryptomattes [\#2832](https://github.com/pypeclub/OpenPype/pull/2832) - Deadline: Remove recreated event [\#2828](https://github.com/pypeclub/OpenPype/pull/2828) - Deadline: Added missing events folder [\#2827](https://github.com/pypeclub/OpenPype/pull/2827) @@ -46,10 +49,6 @@ - Ftrack: Unset task ids from asset versions before tasks are removed [\#2800](https://github.com/pypeclub/OpenPype/pull/2800) - Slack: fail gracefully if slack exception [\#2798](https://github.com/pypeclub/OpenPype/pull/2798) - Flame: Fix version string in default settings [\#2783](https://github.com/pypeclub/OpenPype/pull/2783) -- Houdini: Fix open last workfile [\#2767](https://github.com/pypeclub/OpenPype/pull/2767) -- Maya: Fix `unique\_namespace` when in an namespace that is empty [\#2759](https://github.com/pypeclub/OpenPype/pull/2759) -- Maya: Remove some unused code [\#2709](https://github.com/pypeclub/OpenPype/pull/2709) -- Multiple hosts: unify menu style across hosts [\#2693](https://github.com/pypeclub/OpenPype/pull/2693) **Merged pull requests:** diff --git a/openpype/__init__.py b/openpype/__init__.py index 63ad81d3ca..7d046e4ef4 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -5,6 +5,7 @@ import platform import functools import logging +from openpype.pipeline import LegacyCreator from .settings import get_project_settings from .lib import ( Anatomy, @@ -59,10 +60,15 @@ def patched_discover(superclass): """ # run original discover and get plugins plugins = _original_discover(superclass) + filtered_plugins = [ + plugin + for plugin in plugins + if issubclass(plugin, superclass) + ] - set_plugin_attributes_from_settings(plugins, superclass) + set_plugin_attributes_from_settings(filtered_plugins, superclass) - return plugins + return filtered_plugins @import_wrapper @@ -114,7 +120,7 @@ def install(): pyblish.register_plugin_path(path) avalon.register_plugin_path(avalon.Loader, path) - avalon.register_plugin_path(avalon.Creator, path) + avalon.register_plugin_path(LegacyCreator, path) avalon.register_plugin_path(avalon.InventoryAction, path) # apply monkey patched discover to original one diff --git a/openpype/api.py b/openpype/api.py index 51854492ab..b692b36065 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -45,9 +45,6 @@ from .lib.avalon_context import ( from . import resources from .plugin import ( - PypeCreatorMixin, - Creator, - Extractor, ValidatePipelineOrder, @@ -89,9 +86,6 @@ __all__ = [ # Resources "resources", - # Pype creator mixin - "PypeCreatorMixin", - "Creator", # plugin classes "Extractor", # ordering diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 96d04aad14..41d2417818 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -9,6 +9,7 @@ from avalon import io, pipeline from openpype import lib from openpype.api import Logger +from openpype.pipeline import LegacyCreator import openpype.hosts.aftereffects from openpype.lib import register_event_callback @@ -67,7 +68,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( @@ -80,7 +81,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def on_pyblish_instance_toggled(instance, old_value, new_value): diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 8dfc85cdc8..41efb4b0ba 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,13 +1,12 @@ -from avalon.api import CreatorError - -import openpype.api +from openpype.pipeline import create +from openpype.pipeline import CreatorError from openpype.hosts.aftereffects.api import ( get_stub, list_instances ) -class CreateRender(openpype.api.Creator): +class CreateRender(create.LegacyCreator): """Render folder for publish. Creates subsets in format 'familyTaskSubsetname', diff --git a/openpype/hosts/aftereffects/plugins/publish/help/validate_instance_asset.xml b/openpype/hosts/aftereffects/plugins/publish/help/validate_instance_asset.xml new file mode 100644 index 0000000000..13f03a9b9a --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/help/validate_instance_asset.xml @@ -0,0 +1,21 @@ + + + +Subset context + +## Invalid subset context + +Context of the given subset doesn't match your current scene. + +### How to repair? + +You can fix this with "repair" button on the right. + + +### __Detailed Info__ (optional) + +This might happen if you are reuse old workfile and open it in different context. +(Eg. you created subset "renderCompositingDefault" from asset "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing subset for "Robot" asset stayed in the workfile.) + + + \ No newline at end of file diff --git a/openpype/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml b/openpype/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml new file mode 100644 index 0000000000..36fa90456e --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml @@ -0,0 +1,35 @@ + + + +Scene setting + +## Invalid scene setting found + +One of the settings in a scene doesn't match to asset settings in database. + +{invalid_setting_str} + +### How to repair? + +Change values for {invalid_keys_str} in the scene OR change them in the asset database if they are wrong there. + + +### __Detailed Info__ (optional) + +This error is shown when for example resolution in the scene doesn't match to resolution set on the asset in the database. +Either value in the database or in the scene is wrong. + + + +Scene file doesn't exist + +## Scene file doesn't exist + +Collected scene {scene_url} doesn't exist. + +### How to repair? + +Re-save file, start publish from the beginning again. + + + \ No newline at end of file diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py index 71c1750457..37cecfbcc4 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py @@ -1,6 +1,7 @@ from avalon import api import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError from openpype.hosts.aftereffects.api import get_stub @@ -53,9 +54,8 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin): current_asset = api.Session["AVALON_ASSET"] msg = ( f"Instance asset {instance_asset} is not the same " - f"as current context {current_asset}. PLEASE DO:\n" - f"Repair with 'A' action to use '{current_asset}'.\n" - f"If that's not correct value, close workfile and " - f"reopen via Workfiles!" + f"as current context {current_asset}." ) - assert instance_asset == current_asset, msg + + if instance_asset != current_asset: + raise PublishXmlValidationError(self, msg) diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py index 5ae391e230..273ccd295e 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py @@ -5,6 +5,7 @@ import re import pyblish.api +from openpype.pipeline import PublishXmlValidationError from openpype.hosts.aftereffects.api import get_asset_settings @@ -99,12 +100,14 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): self.log.info("current_settings:: {}".format(current_settings)) invalid_settings = [] + invalid_keys = set() for key, value in expected_settings.items(): if value != current_settings[key]: invalid_settings.append( "{} expected: {} found: {}".format(key, value, current_settings[key]) ) + invalid_keys.add(key) if ((expected_settings.get("handleStart") or expected_settings.get("handleEnd")) @@ -116,7 +119,27 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): msg = "Found invalid settings:\n{}".format( "\n".join(invalid_settings) ) - assert not invalid_settings, msg - assert os.path.exists(instance.data.get("source")), ( - "Scene file not found (saved under wrong name)" - ) + + if invalid_settings: + invalid_keys_str = ",".join(invalid_keys) + break_str = "
" + invalid_setting_str = "Found invalid settings:
{}".\ + format(break_str.join(invalid_settings)) + + formatting_data = { + "invalid_setting_str": invalid_setting_str, + "invalid_keys_str": invalid_keys_str + } + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) + + if not os.path.exists(instance.data.get("source")): + scene_url = instance.data.get("source") + msg = "Scene file {} not found (saved under wrong name)".format( + scene_url + ) + formatting_data = { + "scene_url": scene_url + } + raise PublishXmlValidationError(self, msg, key="file_not_found", + formatting_data=formatting_data) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index d1e7df3a93..efa08ba59e 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -14,6 +14,7 @@ import avalon.api from avalon import io, schema from avalon.pipeline import AVALON_CONTAINER_ID +from openpype.pipeline import LegacyCreator from openpype.api import Logger from openpype.lib import ( register_event_callback, @@ -50,7 +51,7 @@ def install(): pyblish.api.register_plugin_path(str(PUBLISH_PATH)) avalon.api.register_plugin_path(avalon.api.Loader, str(LOAD_PATH)) - avalon.api.register_plugin_path(avalon.api.Creator, str(CREATE_PATH)) + avalon.api.register_plugin_path(LegacyCreator, str(CREATE_PATH)) lib.append_user_scripts() @@ -72,7 +73,7 @@ def uninstall(): pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) avalon.api.deregister_plugin_path(avalon.api.Loader, str(LOAD_PATH)) - avalon.api.deregister_plugin_path(avalon.api.Creator, str(CREATE_PATH)) + avalon.api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH)) if not IS_HEADLESS: ops.unregister() diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 8c9ab9a27f..20d1e4c8db 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional import bpy import avalon.api -from openpype.api import PypeCreatorMixin +from openpype.pipeline import LegacyCreator from .pipeline import AVALON_CONTAINERS from .ops import ( MainThreadItem, @@ -129,7 +129,7 @@ def deselect_all(): bpy.context.view_layer.objects.active = active -class Creator(PypeCreatorMixin, avalon.api.Creator): +class Creator(LegacyCreator): """Base class for Creator plug-ins.""" defaults = ['Main'] diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index 8029c38b4a..7f8ae610c6 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -8,6 +8,7 @@ import bpy from avalon import api from openpype import lib +from openpype.pipeline import legacy_create from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import ( AVALON_CONTAINERS, @@ -159,7 +160,7 @@ class BlendLayoutLoader(plugin.AssetLoader): raise ValueError("Creator plugin \"CreateAnimation\" was " "not found.") - api.create( + legacy_create( creator_plugin, name=local_obj.name.split(':')[-1] + "_animation", asset=asset, diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 0a5bdeecaa..5b5f9ab83d 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -8,7 +8,6 @@ from typing import Dict, Optional import bpy from avalon import api -from openpype import lib from openpype.hosts.blender.api.pipeline import ( AVALON_INSTANCES, AVALON_CONTAINERS, @@ -118,7 +117,7 @@ class JsonLayoutLoader(plugin.AssetLoader): # raise ValueError("Creator plugin \"CreateCamera\" was " # "not found.") - # api.create( + # legacy_create( # creator_plugin, # name="camera", # # name=f"{unique_number}_{subset}_animation", diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index eb6d273a51..eacabd3447 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -9,6 +9,7 @@ import bpy from avalon import api from avalon.blender import lib as avalon_lib from openpype import lib +from openpype.pipeline import legacy_create from openpype.hosts.blender.api import plugin from openpype.hosts.blender.api.pipeline import ( AVALON_CONTAINERS, @@ -248,7 +249,7 @@ class BlendRigLoader(plugin.AssetLoader): animation_asset = options.get('animation_asset') - api.create( + legacy_create( creator_plugin, name=namespace + "_animation", # name=f"{unique_number}_{subset}_animation", diff --git a/openpype/hosts/celaction/plugins/publish/submit_celaction_deadline.py b/openpype/hosts/celaction/plugins/publish/submit_celaction_deadline.py index fd958d11a3..ea109e9445 100644 --- a/openpype/hosts/celaction/plugins/publish/submit_celaction_deadline.py +++ b/openpype/hosts/celaction/plugins/publish/submit_celaction_deadline.py @@ -1,9 +1,9 @@ import os +import re import json import getpass -from avalon.vendor import requests -import re +import requests import pyblish.api diff --git a/openpype/hosts/flame/api/pipeline.py b/openpype/hosts/flame/api/pipeline.py index af071439ef..f802cf160b 100644 --- a/openpype/hosts/flame/api/pipeline.py +++ b/openpype/hosts/flame/api/pipeline.py @@ -7,6 +7,7 @@ from avalon import api as avalon from avalon.pipeline import AVALON_CONTAINER_ID from pyblish import api as pyblish from openpype.api import Logger +from openpype.pipeline import LegacyCreator from .lib import ( set_segment_data_marker, set_publish_attribute, @@ -33,7 +34,7 @@ def install(): pyblish.register_host("flame") pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) log.info("OpenPype Flame plug-ins registred ...") @@ -48,7 +49,7 @@ def uninstall(): log.info("Deregistering Flame plug-ins..") pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) # register callback for switching publishable diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py index ec49db1601..5221701a2f 100644 --- a/openpype/hosts/flame/api/plugin.py +++ b/openpype/hosts/flame/api/plugin.py @@ -2,11 +2,12 @@ import os import re import shutil import sys -from avalon.vendor import qargparse from xml.etree import ElementTree as ET import six +import qargparse from Qt import QtWidgets, QtCore import openpype.api as openpype +from openpype.pipeline import LegacyCreator from openpype import style import avalon.api as avalon from . import ( @@ -299,7 +300,7 @@ class Spacer(QtWidgets.QWidget): self.setLayout(layout) -class Creator(openpype.Creator): +class Creator(LegacyCreator): """Creator class wrapper """ clip_color = constants.COLOR_MAP["purple"] diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 64dda0bc8a..5ac56fcbed 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -11,6 +11,7 @@ import avalon.api from avalon.pipeline import AVALON_CONTAINER_ID from openpype.api import Logger +from openpype.pipeline import LegacyCreator import openpype.hosts.fusion log = Logger().get_logger(__name__) @@ -63,7 +64,7 @@ def install(): log.info("Registering Fusion plug-ins..") avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) pyblish.api.register_callback( @@ -87,7 +88,7 @@ def uninstall(): log.info("Deregistering Fusion plug-ins..") avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.deregister_plugin_path( avalon.api.InventoryAction, INVENTORY_PATH ) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 04717f4746..ff8bdb21ef 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,13 +1,13 @@ import os -import openpype.api +from openpype.pipeline import create from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk ) -class CreateOpenEXRSaver(openpype.api.Creator): +class CreateOpenEXRSaver(create.LegacyCreator): name = "openexrDefault" label = "Create OpenEXR Saver" diff --git a/openpype/hosts/fusion/scripts/set_rendermode.py b/openpype/hosts/fusion/scripts/set_rendermode.py index 77a2d8e945..f0638e4fe3 100644 --- a/openpype/hosts/fusion/scripts/set_rendermode.py +++ b/openpype/hosts/fusion/scripts/set_rendermode.py @@ -1,5 +1,5 @@ from Qt import QtWidgets -from avalon.vendor import qtawesome +import qtawesome from openpype.hosts.fusion.api import get_current_comp diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/utility_scripts/switch_ui.py index afb39f7041..d9eeae25ea 100644 --- a/openpype/hosts/fusion/utility_scripts/switch_ui.py +++ b/openpype/hosts/fusion/utility_scripts/switch_ui.py @@ -6,7 +6,7 @@ from Qt import QtWidgets, QtCore import avalon.api from avalon import io -from avalon.vendor import qtawesome as qta +import qtawesome as qta from openpype import style from openpype.hosts.fusion import api diff --git a/openpype/hosts/harmony/api/pipeline.py b/openpype/hosts/harmony/api/pipeline.py index 8d7ac19eb9..928bf926ff 100644 --- a/openpype/hosts/harmony/api/pipeline.py +++ b/openpype/hosts/harmony/api/pipeline.py @@ -10,6 +10,7 @@ from avalon.pipeline import AVALON_CONTAINER_ID from openpype import lib from openpype.lib import register_event_callback +from openpype.pipeline import LegacyCreator import openpype.hosts.harmony import openpype.hosts.harmony.api as harmony @@ -180,7 +181,7 @@ def install(): pyblish.api.register_host("harmony") pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) log.info(PUBLISH_PATH) # Register callbacks. @@ -194,7 +195,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def on_pyblish_instance_toggled(instance, old_value, new_value): diff --git a/openpype/hosts/harmony/api/plugin.py b/openpype/hosts/harmony/api/plugin.py index d6d61a547a..c55d200d30 100644 --- a/openpype/hosts/harmony/api/plugin.py +++ b/openpype/hosts/harmony/api/plugin.py @@ -1,9 +1,8 @@ -import avalon.api -from openpype.api import PypeCreatorMixin +from openpype.pipeline import LegacyCreator import openpype.hosts.harmony.api as harmony -class Creator(PypeCreatorMixin, avalon.api.Creator): +class Creator(LegacyCreator): """Creator plugin to create instances in Harmony. By default a Composite node is created to support any number of nodes in diff --git a/openpype/hosts/harmony/plugins/publish/help/validate_audio.xml b/openpype/hosts/harmony/plugins/publish/help/validate_audio.xml new file mode 100644 index 0000000000..e9a183c675 --- /dev/null +++ b/openpype/hosts/harmony/plugins/publish/help/validate_audio.xml @@ -0,0 +1,15 @@ + + + +Missing audio file + +## Cannot locate linked audio file + +Audio file at {audio_url} cannot be found. + +### How to repair? + +Copy audio file to the highlighted location or remove audio link in the workfile. + + + \ No newline at end of file diff --git a/openpype/hosts/harmony/plugins/publish/help/validate_instances.xml b/openpype/hosts/harmony/plugins/publish/help/validate_instances.xml new file mode 100644 index 0000000000..3b040e8ea8 --- /dev/null +++ b/openpype/hosts/harmony/plugins/publish/help/validate_instances.xml @@ -0,0 +1,25 @@ + + + +Subset context + +## Invalid subset context + +Asset name found '{found}' in subsets, expected '{expected}'. + +### How to repair? + +You can fix this with `Repair` button on the right. This will use '{expected}' asset name and overwrite '{found}' asset name in scene metadata. + +After that restart `Publish` with a `Reload button`. + +If this is unwanted, close workfile and open again, that way different asset value would be used for context information. + + +### __Detailed Info__ (optional) + +This might happen if you are reuse old workfile and open it in different context. +(Eg. you created subset "renderCompositingDefault" from asset "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing subset for "Robot" asset stayed in the workfile.) + + + \ No newline at end of file diff --git a/openpype/hosts/harmony/plugins/publish/help/validate_scene_settings.xml b/openpype/hosts/harmony/plugins/publish/help/validate_scene_settings.xml new file mode 100644 index 0000000000..36fa90456e --- /dev/null +++ b/openpype/hosts/harmony/plugins/publish/help/validate_scene_settings.xml @@ -0,0 +1,35 @@ + + + +Scene setting + +## Invalid scene setting found + +One of the settings in a scene doesn't match to asset settings in database. + +{invalid_setting_str} + +### How to repair? + +Change values for {invalid_keys_str} in the scene OR change them in the asset database if they are wrong there. + + +### __Detailed Info__ (optional) + +This error is shown when for example resolution in the scene doesn't match to resolution set on the asset in the database. +Either value in the database or in the scene is wrong. + + + +Scene file doesn't exist + +## Scene file doesn't exist + +Collected scene {scene_url} doesn't exist. + +### How to repair? + +Re-save file, start publish from the beginning again. + + + \ No newline at end of file diff --git a/openpype/hosts/harmony/plugins/publish/validate_audio.py b/openpype/hosts/harmony/plugins/publish/validate_audio.py index d183dadb91..cb6b2307cd 100644 --- a/openpype/hosts/harmony/plugins/publish/validate_audio.py +++ b/openpype/hosts/harmony/plugins/publish/validate_audio.py @@ -4,6 +4,8 @@ import pyblish.api import openpype.hosts.harmony.api as harmony +from openpype.pipeline import PublishXmlValidationError + class ValidateAudio(pyblish.api.InstancePlugin): """Ensures that there is an audio file in the scene. @@ -42,4 +44,9 @@ class ValidateAudio(pyblish.api.InstancePlugin): msg = "You are missing audio file:\n{}".format(audio_path) - assert os.path.isfile(audio_path), msg + formatting_data = { + "audio_url": audio_path + } + if os.path.isfile(audio_path): + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/hosts/harmony/plugins/publish/validate_instances.py b/openpype/hosts/harmony/plugins/publish/validate_instances.py index 1ba65573fc..373ef94cc3 100644 --- a/openpype/hosts/harmony/plugins/publish/validate_instances.py +++ b/openpype/hosts/harmony/plugins/publish/validate_instances.py @@ -2,6 +2,7 @@ import os import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError import openpype.hosts.harmony.api as harmony @@ -45,4 +46,11 @@ class ValidateInstance(pyblish.api.InstancePlugin): "Instance asset is not the same as current asset:" f"\nInstance: {instance_asset}\nCurrent: {current_asset}" ) - assert instance_asset == current_asset, msg + + formatting_data = { + "found": instance_asset, + "expected": current_asset + } + if instance_asset != current_asset: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py b/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py index efd9bbb212..4c3a6c4465 100644 --- a/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/harmony/plugins/publish/validate_scene_settings.py @@ -7,7 +7,7 @@ import re import pyblish.api import openpype.hosts.harmony.api as harmony -import openpype.hosts.harmony +from openpype.pipeline import PublishXmlValidationError class ValidateSceneSettingsRepair(pyblish.api.Action): @@ -19,12 +19,12 @@ class ValidateSceneSettingsRepair(pyblish.api.Action): def process(self, context, plugin): """Repair action entry point.""" - expected = openpype.hosts.harmony.api.get_asset_settings() + expected = harmony.get_asset_settings() asset_settings = _update_frames(dict.copy(expected)) asset_settings["frameStart"] = 1 asset_settings["frameEnd"] = asset_settings["frameEnd"] + \ asset_settings["handleEnd"] - openpype.hosts.harmony.api.set_scene_settings(asset_settings) + harmony.set_scene_settings(asset_settings) if not os.path.exists(context.data["scenePath"]): self.log.info("correcting scene name") scene_dir = os.path.dirname(context.data["currentFile"]) @@ -55,7 +55,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" - expected_settings = openpype.hosts.harmony.api.get_asset_settings() + expected_settings = harmony.get_asset_settings() self.log.info("scene settings from DB:".format(expected_settings)) expected_settings = _update_frames(dict.copy(expected_settings)) @@ -102,13 +102,13 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): self.log.debug("current scene settings {}".format(current_settings)) invalid_settings = [] + invalid_keys = set() for key, value in expected_settings.items(): if value != current_settings[key]: - invalid_settings.append({ - "name": key, - "expected": value, - "current": current_settings[key] - }) + invalid_settings.append( + "{} expected: {} found: {}".format(key, value, + current_settings[key])) + invalid_keys.add(key) if ((expected_settings["handleStart"] or expected_settings["handleEnd"]) @@ -120,10 +120,30 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): msg = "Found invalid settings:\n{}".format( json.dumps(invalid_settings, sort_keys=True, indent=4) ) - assert not invalid_settings, msg - assert os.path.exists(instance.context.data.get("scenePath")), ( - "Scene file not found (saved under wrong name)" - ) + + if invalid_settings: + invalid_keys_str = ",".join(invalid_keys) + break_str = "
" + invalid_setting_str = "Found invalid settings:
{}".\ + format(break_str.join(invalid_settings)) + + formatting_data = { + "invalid_setting_str": invalid_setting_str, + "invalid_keys_str": invalid_keys_str + } + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) + + scene_url = instance.context.data.get("scenePath") + if not os.path.exists(scene_url): + msg = "Scene file {} not found (saved under wrong name)".format( + scene_url + ) + formatting_data = { + "scene_url": scene_url + } + raise PublishXmlValidationError(self, msg, key="file_not_found", + formatting_data=formatting_data) def _update_frames(expected_settings): diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index cbcaf23994..5cb23ea355 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -9,6 +9,7 @@ from avalon import api as avalon from avalon import schema from pyblish import api as pyblish from openpype.api import Logger +from openpype.pipeline import LegacyCreator from openpype.tools.utils import host_tools from . import lib, menu, events @@ -45,7 +46,7 @@ def install(): pyblish.register_host("hiero") pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) # register callback for switching publishable @@ -67,7 +68,7 @@ def uninstall(): pyblish.deregister_host("hiero") pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) # register callback for switching publishable pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/hiero/api/plugin.py b/openpype/hosts/hiero/api/plugin.py index 3506af2d6a..53928aca41 100644 --- a/openpype/hosts/hiero/api/plugin.py +++ b/openpype/hosts/hiero/api/plugin.py @@ -1,12 +1,15 @@ -import re import os +import re +from copy import deepcopy + import hiero + from Qt import QtWidgets, QtCore -from avalon.vendor import qargparse +import qargparse import avalon.api as avalon import openpype.api as openpype +from openpype.pipeline import LegacyCreator from . import lib -from copy import deepcopy log = openpype.Logger().get_logger(__name__) @@ -589,7 +592,7 @@ class ClipLoader: return track_item -class Creator(openpype.Creator): +class Creator(LegacyCreator): """Creator class wrapper """ clip_color = "Purple" diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index bbb7a7c512..e34b8811b9 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -11,6 +11,7 @@ import avalon.api from avalon.pipeline import AVALON_CONTAINER_ID from avalon.lib import find_submodule +from openpype.pipeline import LegacyCreator import openpype.hosts.houdini from openpype.hosts.houdini.api import lib @@ -50,7 +51,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) log.info("Installing callbacks ... ") # register_event_callback("init", on_init) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 4967d01d43..2bbb65aa05 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -2,11 +2,12 @@ """Houdini specific Avalon/Pyblish plugin definitions.""" import sys import six -import avalon.api -from avalon.api import CreatorError import hou -from openpype.api import PypeCreatorMixin +from openpype.pipeline import ( + CreatorError, + LegacyCreator +) from .lib import imprint @@ -14,7 +15,7 @@ class OpenPypeCreatorError(CreatorError): pass -class Creator(PypeCreatorMixin, avalon.api.Creator): +class Creator(LegacyCreator): """Creator plugin to create instances in Houdini To support the wide range of node types for render output (Alembic, VDB, diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 07743b7fc4..bdb4e5dfa8 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -20,6 +20,7 @@ from openpype.lib import ( emit_event ) from openpype.lib.path_tools import HostDirmap +from openpype.pipeline import LegacyCreator from openpype.hosts.maya.lib import copy_workspace_mel from . import menu, lib @@ -53,7 +54,7 @@ def install(): pyblish.api.register_host("maya") avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) log.info(PUBLISH_PATH) @@ -182,7 +183,7 @@ def uninstall(): pyblish.api.deregister_host("maya") avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.deregister_plugin_path( avalon.api.InventoryAction, INVENTORY_PATH ) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index bdb8fcf13a..d99e8d25b7 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -2,9 +2,10 @@ import os from maya import cmds +import qargparse + from avalon import api -from avalon.vendor import qargparse -from openpype.api import PypeCreatorMixin +from openpype.pipeline import LegacyCreator from .pipeline import containerise from . import lib @@ -77,7 +78,7 @@ def get_reference_node_parents(ref): return parents -class Creator(PypeCreatorMixin, api.Creator): +class Creator(LegacyCreator): defaults = ['Main'] def process(self): diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 743ec26778..9002ae3876 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -19,9 +19,9 @@ from openpype.api import ( get_project_settings, get_asset) from openpype.modules import ModulesManager +from openpype.pipeline import CreatorError from avalon.api import Session -from avalon.api import CreatorError class CreateRender(plugin.Creator): diff --git a/openpype/hosts/maya/plugins/create/create_vrayscene.py b/openpype/hosts/maya/plugins/create/create_vrayscene.py index f2096d902e..fa9c59e016 100644 --- a/openpype/hosts/maya/plugins/create/create_vrayscene.py +++ b/openpype/hosts/maya/plugins/create/create_vrayscene.py @@ -19,10 +19,10 @@ from openpype.api import ( get_project_settings ) +from openpype.pipeline import CreatorError from openpype.modules import ModulesManager from avalon.api import Session -from avalon.api import CreatorError class CreateVRayScene(plugin.Creator): diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 0565b0b95c..25db5fb1e5 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -3,6 +3,7 @@ from maya import cmds from avalon import api from openpype.api import get_project_settings from openpype.lib import get_creator_by_name +from openpype.pipeline import legacy_create import openpype.hosts.maya.api.plugin from openpype.hosts.maya.api.lib import maintained_selection @@ -151,7 +152,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): creator_plugin = get_creator_by_name(self.animation_creator_name) with maintained_selection(): cmds.select([output, controls] + roots, noExpand=True) - api.create( + legacy_create( creator_plugin, name=namespace, asset=asset, diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py index 099c020093..6d5544103d 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -174,7 +174,7 @@ class LoadVDBtoVRay(api.Loader): fname = files[0] else: # Sequence - from avalon.vendor import clique + import clique # todo: check support for negative frames as input collections, remainder = clique.assemble(files) assert len(collections) == 1, ( diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 419cfb1ac2..348c9680b2 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -15,6 +15,7 @@ from openpype.api import ( get_current_project_settings ) from openpype.lib import register_event_callback +from openpype.pipeline import LegacyCreator from openpype.tools.utils import host_tools from .command import viewer_update_and_undo_stop @@ -99,7 +100,7 @@ def install(): log.info("Registering Nuke plug-ins..") pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH) # Register Avalon event for workfiles loading. @@ -125,7 +126,7 @@ def uninstall(): pyblish.deregister_host("nuke") pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) pyblish.api.deregister_callback( "instanceToggled", on_pyblish_instance_toggled) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 3e61caedf9..ff186cd685 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -6,10 +6,8 @@ import nuke import avalon.api -from openpype.api import ( - get_current_project_settings, - PypeCreatorMixin -) +from openpype.api import get_current_project_settings +from openpype.pipeline import LegacyCreator from .lib import ( Knobby, check_subsetname_exists, @@ -20,7 +18,7 @@ from .lib import ( ) -class OpenPypeCreator(PypeCreatorMixin, avalon.api.Creator): +class OpenPypeCreator(LegacyCreator): """Pype Nuke Creator class wrapper""" node_color = "0xdfea5dff" @@ -152,6 +150,7 @@ class ExporterReview(object): """ data = None + publish_on_farm = False def __init__(self, klass, @@ -210,6 +209,9 @@ class ExporterReview(object): if self.multiple_presets: repre["outputName"] = self.name + if self.publish_on_farm: + repre["tags"].append("publish_on_farm") + self.data["representations"].append(repre) def get_view_input_process_node(self): @@ -446,6 +448,7 @@ class ExporterReviewMov(ExporterReview): return path def generate_mov(self, farm=False, **kwargs): + self.publish_on_farm = farm reformat_node_add = kwargs["reformat_node_add"] reformat_node_config = kwargs["reformat_node_config"] bake_viewer_process = kwargs["bake_viewer_process"] @@ -563,7 +566,7 @@ class ExporterReviewMov(ExporterReview): # ---------- end nodes creation # ---------- render or save to nk - if farm: + if self.publish_on_farm: nuke.scriptSave() path_nk = self.save_file() self.data.update({ @@ -573,11 +576,12 @@ class ExporterReviewMov(ExporterReview): }) else: self.render(write_node.name()) - # ---------- generate representation data - self.get_representation_data( - tags=["review", "delete"] + add_tags, - range=True - ) + + # ---------- generate representation data + self.get_representation_data( + tags=["review", "delete"] + add_tags, + range=True + ) self.log.debug("Representation... `{}`".format(self.data)) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 21b7a6a816..a253ba4a9d 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -1,5 +1,5 @@ import nuke -from avalon.vendor import qargparse +import qargparse from avalon import api, io from openpype.hosts.nuke.api.lib import ( diff --git a/openpype/hosts/nuke/plugins/load/load_image.py b/openpype/hosts/nuke/plugins/load/load_image.py index d36226b139..27c634ec57 100644 --- a/openpype/hosts/nuke/plugins/load/load_image.py +++ b/openpype/hosts/nuke/plugins/load/load_image.py @@ -1,7 +1,6 @@ -import re import nuke -from avalon.vendor import qargparse +import qargparse from avalon import api, io from openpype.hosts.nuke.api.lib import ( diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 1071834497..544b9e04da 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -130,9 +130,11 @@ class ExtractReviewDataMov(openpype.api.Extractor): }) else: data = exporter.generate_mov(**o_data) - generated_repres.extend(data["representations"]) - self.log.info(generated_repres) + # add representation generated by exporter + generated_repres.extend(data["representations"]) + self.log.debug( + "__ generated_repres: {}".format(generated_repres)) if generated_repres: # assign to representations diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index 4cc2aa2c78..17ea957066 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -16,7 +16,6 @@ from .pipeline import ( ) from .plugin import ( PhotoshopLoader, - Creator, get_unique_layer_name ) from .workio import ( @@ -42,11 +41,11 @@ __all__ = [ "list_instances", "remove_instance", "install", + "uninstall", "containerise", # Plugin "PhotoshopLoader", - "Creator", "get_unique_layer_name", # workfiles diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index e424400465..1be8129aa1 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -6,7 +6,9 @@ import avalon.api from avalon import pipeline, io from openpype.api import Logger + from openpype.lib import register_event_callback +from openpype.pipeline import LegacyCreator import openpype.hosts.photoshop from . import lib @@ -68,7 +70,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( @@ -81,7 +83,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def ls(): diff --git a/openpype/hosts/photoshop/api/plugin.py b/openpype/hosts/photoshop/api/plugin.py index e0db67de2c..c577c67d82 100644 --- a/openpype/hosts/photoshop/api/plugin.py +++ b/openpype/hosts/photoshop/api/plugin.py @@ -33,37 +33,3 @@ class PhotoshopLoader(avalon.api.Loader): @staticmethod def get_stub(): return stub() - - -class Creator(avalon.api.Creator): - """Creator plugin to create instances in Photoshop - - A LayerSet is created to support any number of layers in an instance. If - the selection is used, these layers will be added to the LayerSet. - """ - - def process(self): - # Photoshop can have multiple LayerSets with the same name, which does - # not work with Avalon. - msg = "Instance with name \"{}\" already exists.".format(self.name) - stub = lib.stub() # only after Photoshop is up - for layer in stub.get_layers(): - if self.name.lower() == layer.Name.lower(): - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Warning) - msg.setText(msg) - msg.exec_() - return False - - # Store selection because adding a group will change selection. - with lib.maintained_selection(): - - # Add selection to group. - if (self.options or {}).get("useSelection"): - group = stub.group_selected_layers(self.name) - else: - group = stub.create_group(self.name) - - stub.imprint(group, self.data) - - return group diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 344a53f47e..a001b5f171 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -1,9 +1,9 @@ from Qt import QtWidgets -import openpype.api +from openpype.pipeline import create from openpype.hosts.photoshop import api as photoshop -class CreateImage(openpype.api.Creator): +class CreateImage(create.LegacyCreator): """Image folder for publish.""" name = "imageDefault" diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 6627aded51..12e0503dfc 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -1,7 +1,7 @@ import os +import qargparse from avalon.pipeline import get_representation_path_from_context -from avalon.vendor import qargparse from openpype.hosts.photoshop import api as photoshop from openpype.hosts.photoshop.api import get_unique_layer_name @@ -92,4 +92,3 @@ class ImageFromSequenceLoader(photoshop.PhotoshopLoader): def remove(self, container): """No update possible, not containerized.""" pass - diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 2dc5136c8a..c82545268b 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -9,6 +9,7 @@ from avalon import schema from avalon.pipeline import AVALON_CONTAINER_ID from pyblish import api as pyblish from openpype.api import Logger +from openpype.pipeline import LegacyCreator from . import lib from . import PLUGINS_DIR from openpype.tools.utils import host_tools @@ -42,7 +43,7 @@ def install(): log.info("Registering DaVinci Resovle plug-ins..") avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.register_plugin_path(avalon.InventoryAction, INVENTORY_PATH) # register callback for switching publishable @@ -67,7 +68,7 @@ def uninstall(): log.info("Deregistering DaVinci Resovle plug-ins..") avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) avalon.deregister_plugin_path(avalon.InventoryAction, INVENTORY_PATH) # register callback for switching publishable diff --git a/openpype/hosts/resolve/api/plugin.py b/openpype/hosts/resolve/api/plugin.py index 8612cf82ec..e7793d6e95 100644 --- a/openpype/hosts/resolve/api/plugin.py +++ b/openpype/hosts/resolve/api/plugin.py @@ -1,12 +1,15 @@ import re import uuid + +import qargparse +from Qt import QtWidgets, QtCore + from avalon import api import openpype.api as pype +from openpype.pipeline import LegacyCreator from openpype.hosts import resolve -from avalon.vendor import qargparse from . import lib -from Qt import QtWidgets, QtCore class CreatorWidget(QtWidgets.QDialog): @@ -493,7 +496,7 @@ class TimelineItemLoader(api.Loader): pass -class Creator(pype.PypeCreatorMixin, api.Creator): +class Creator(LegacyCreator): """Creator class wrapper """ marker_color = "Purple" diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_editorial_resources.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_editorial_resources.xml new file mode 100644 index 0000000000..803de6bf11 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_editorial_resources.xml @@ -0,0 +1,17 @@ + + + +Missing source video file + +## No attached video file found + +Process expects presence of source video file with same name prefix as an editorial file in same folder. +(example `simple_editorial_setup_Layer1.edl` expects `simple_editorial_setup.mp4` in same folder) + + +### How to repair? + +Copy source video file to the folder next to `.edl` file. (On a disk, do not put it into Standalone Publisher.) + + + diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_frame_ranges.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_frame_ranges.xml new file mode 100644 index 0000000000..933df1c7c5 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_frame_ranges.xml @@ -0,0 +1,15 @@ + + + +Invalid frame range + +## Invalid frame range + +Expected duration or '{duration}' frames set in database, workfile contains only '{found}' frames. + +### How to repair? + +Modify configuration in the database or tweak frame range in the workfile. + + + \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_shot_duplicates.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_shot_duplicates.xml new file mode 100644 index 0000000000..77b8727162 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_shot_duplicates.xml @@ -0,0 +1,15 @@ + + + +Duplicate shots + +## Duplicate shot names + +Process contains duplicated shot names '{duplicates_str}'. + +### How to repair? + +Remove shot duplicates. + + + \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_sources.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_sources.xml new file mode 100644 index 0000000000..d527d2173e --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_sources.xml @@ -0,0 +1,16 @@ + + + +Files not found + +## Source files not found + +Process contains duplicated shot names: +'{files_not_found}' + +### How to repair? + +Add missing files or run Publish again to collect new publishable files. + + + \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_task_existence.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_task_existence.xml new file mode 100644 index 0000000000..a943f560d0 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_task_existence.xml @@ -0,0 +1,16 @@ + + + +Task not found + +## Task not found in database + +Process contains tasks that don't exist in database: +'{task_not_found}' + +### How to repair? + +Remove set task or add task into database into proper place. + + + \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_batch.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_batch.xml new file mode 100644 index 0000000000..a645df8d02 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_batch.xml @@ -0,0 +1,15 @@ + + + +No texture files found + +## Batch doesn't contain texture files + +Batch must contain at least one texture file. + +### How to repair? + +Add texture file to the batch or check name if it follows naming convention to match texture files to the batch. + + + \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_has_workfile.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_has_workfile.xml new file mode 100644 index 0000000000..077987a96d --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_has_workfile.xml @@ -0,0 +1,15 @@ + + + +No workfile found + +## Batch should contain workfile + +It is expected that published contains workfile that served as a source for textures. + +### How to repair? + +Add workfile to the batch, or disable this validator if you do not want workfile published. + + + \ No newline at end of file diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_name.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_name.xml new file mode 100644 index 0000000000..2610917736 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_name.xml @@ -0,0 +1,32 @@ + + + +Asset name not found + +## Couldn't parse asset name from a file + +Unable to parse asset name from '{file_name}'. File name doesn't match configured naming convention. + +### How to repair? + +Check Settings: project_settings/standalonepublisher/publish/CollectTextures for naming convention. + + +### __Detailed Info__ (optional) + +This error happens when parsing cannot figure out name of asset texture files belong under. + + + +Missing keys + +## Texture file name is missing some required keys + +Texture '{file_name}' is missing values for {missing_str} keys. + +### How to repair? + +Fix name of texture file and Publish again. + + + diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_versions.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_versions.xml new file mode 100644 index 0000000000..1e536e604f --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_versions.xml @@ -0,0 +1,35 @@ + + + +Texture version + +## Texture version mismatch with workfile + +Workfile '{file_name}' version doesn't match with '{version}' of a texture. + +### How to repair? + +Rename either workfile or texture to contain matching versions + + +### __Detailed Info__ (optional) + +This might happen if you are trying to publish textures for older version of workfile (or the other way). +(Eg. publishing 'workfile_v001' and 'texture_file_v002') + + + +Too many versions + +## Too many versions published at same time + +It is currently expected to publish only batch with single version. + +Found {found} versions. + +### How to repair? + +Please remove files with different version and split publishing into multiple steps. + + + diff --git a/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_workfiles.xml b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_workfiles.xml new file mode 100644 index 0000000000..8187eb0bc8 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/help/validate_texture_workfiles.xml @@ -0,0 +1,23 @@ + + + +No secondary workfile + +## No secondary workfile found + +Current process expects that primary workfile (for example with a extension '{extension}') will contain also 'secondary' workfile. + +Secondary workfile for '{file_name}' wasn't found. + +### How to repair? + +Attach secondary workfile or disable this validator and Publish again. + + +### __Detailed Info__ (optional) + +This process was implemented for a possible use case of first workfile coming from Mari, secondary workfile for textures from Substance. +Publish should contain both if primary workfile is present. + + + diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py index 6759b87ceb..afb828474d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_editorial_resources.py @@ -1,5 +1,6 @@ import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateEditorialResources(pyblish.api.InstancePlugin): @@ -19,5 +20,7 @@ class ValidateEditorialResources(pyblish.api.InstancePlugin): f"Instance: {instance}, Families: " f"{[instance.data['family']] + instance.data['families']}") check_file = instance.data["editorialSourcePath"] - msg = f"Missing \"{check_file}\"." - assert check_file, msg + msg = "Missing source video file." + + if not check_file: + raise PublishXmlValidationError(self, msg) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py index 943cb73b98..005157af62 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py @@ -1,8 +1,10 @@ import re import pyblish.api + import openpype.api from openpype import lib +from openpype.pipeline import PublishXmlValidationError class ValidateFrameRange(pyblish.api.InstancePlugin): @@ -48,9 +50,15 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): files = [files] frames = len(files) - err_msg = "Frame duration from DB:'{}' ". format(int(duration)) +\ - " doesn't match number of files:'{}'".format(frames) +\ - " Please change frame range for Asset or limit no. of files" - assert frames == duration, err_msg + msg = "Frame duration from DB:'{}' ". format(int(duration)) +\ + " doesn't match number of files:'{}'".format(frames) +\ + " Please change frame range for Asset or limit no. of files" - self.log.debug("Valid ranges {} - {}".format(int(duration), frames)) + formatting_data = {"duration": duration, + "found": frames} + if frames != duration: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) + + self.log.debug("Valid ranges expected '{}' - found '{}'". + format(int(duration), frames)) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py index 85ec9379ce..fe655f6b74 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_shot_duplicates.py @@ -1,6 +1,7 @@ import pyblish.api -import openpype.api +import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateShotDuplicates(pyblish.api.ContextPlugin): """Validating no duplicate names are in context.""" @@ -20,4 +21,8 @@ class ValidateShotDuplicates(pyblish.api.ContextPlugin): shot_names.append(name) msg = "There are duplicate shot names:\n{}".format(duplicate_names) - assert not duplicate_names, msg + + formatting_data = {"duplicates_str": ','.join(duplicate_names)} + if duplicate_names: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py index eec675e97f..316f58988f 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_sources.py @@ -1,8 +1,10 @@ -import pyblish.api -import openpype.api - import os +import pyblish.api + +import openpype.api +from openpype.pipeline import PublishXmlValidationError + class ValidateSources(pyblish.api.InstancePlugin): """Validates source files. @@ -11,7 +13,6 @@ class ValidateSources(pyblish.api.InstancePlugin): got deleted between starting of SP and now. """ - order = openpype.api.ValidateContentsOrder label = "Check source files" @@ -22,6 +23,7 @@ class ValidateSources(pyblish.api.InstancePlugin): def process(self, instance): self.log.info("instance {}".format(instance.data)) + missing_files = set() for repre in instance.data.get("representations") or []: files = [] if isinstance(repre["files"], str): @@ -34,4 +36,10 @@ class ValidateSources(pyblish.api.InstancePlugin): file_name) if not os.path.exists(source_file): - raise ValueError("File {} not found".format(source_file)) + missing_files.add(source_file) + + msg = "Files '{}' not found".format(','.join(missing_files)) + formatting_data = {"files_not_found": ' - {}'.join(missing_files)} + if missing_files: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_task_existence.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_task_existence.py index e3b2ae1646..825092c81b 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_task_existence.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_task_existence.py @@ -1,6 +1,8 @@ import pyblish.api from avalon import io +from openpype.pipeline import PublishXmlValidationError + class ValidateTaskExistence(pyblish.api.ContextPlugin): """Validating tasks on instances are filled and existing.""" @@ -53,4 +55,9 @@ class ValidateTaskExistence(pyblish.api.ContextPlugin): "Asset: \"{}\" Task: \"{}\"".format(*missing_pair) ) - raise AssertionError(msg.format("\n".join(pair_msgs))) + msg = msg.format("\n".join(pair_msgs)) + + formatting_data = {"task_not_found": ' - {}'.join(pair_msgs)} + if pair_msgs: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py index d592a4a059..d66fb257bb 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py @@ -1,6 +1,8 @@ import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError + class ValidateTextureBatch(pyblish.api.InstancePlugin): """Validates that some texture files are present.""" @@ -15,8 +17,10 @@ class ValidateTextureBatch(pyblish.api.InstancePlugin): present = False for instance in instance.context: if instance.data["family"] == "textures": - self.log.info("Some textures present.") + self.log.info("At least some textures present.") return - assert present, "No textures found in published batch!" + msg = "No textures found in published batch!" + if not present: + raise PublishXmlValidationError(self, msg) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py index 7cd540668c..0e67464f59 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py @@ -1,5 +1,7 @@ import pyblish.api + import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateTextureHasWorkfile(pyblish.api.InstancePlugin): @@ -17,4 +19,6 @@ class ValidateTextureHasWorkfile(pyblish.api.InstancePlugin): def process(self, instance): wfile = instance.data["versionData"].get("workfile") - assert wfile, "Textures are missing attached workfile" + msg = "Textures are missing attached workfile" + if not wfile: + raise PublishXmlValidationError(self, msg) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py index 4bafe81020..751ad917ca 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py @@ -1,6 +1,7 @@ import pyblish.api -import openpype.api +import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateTextureBatchNaming(pyblish.api.InstancePlugin): """Validates that all instances had properly formatted name.""" @@ -19,9 +20,13 @@ class ValidateTextureBatchNaming(pyblish.api.InstancePlugin): msg = "Couldn't find asset name in '{}'\n".format(file_name) + \ "File name doesn't follow configured pattern.\n" + \ "Please rename the file." - assert "NOT_AVAIL" not in instance.data["asset_build"], msg - instance.data.pop("asset_build") + formatting_data = {"file_name": file_name} + if "NOT_AVAIL" in instance.data["asset_build"]: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) + + instance.data.pop("asset_build") # not needed anymore if instance.data["family"] == "textures": file_name = instance.data["representations"][0]["files"][0] @@ -47,4 +52,10 @@ class ValidateTextureBatchNaming(pyblish.api.InstancePlugin): "Name of the texture file doesn't match expected pattern.\n" + \ "Please rename file(s) {}".format(file_name) - assert not missing_key_values, msg + missing_str = ','.join(["'{}'".format(key) + for key in missing_key_values]) + formatting_data = {"file_name": file_name, + "missing_str": missing_str} + if missing_key_values: + raise PublishXmlValidationError(self, msg, key="missing_values", + formatting_data=formatting_data) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py index 90d0e8e512..84d9def895 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py @@ -1,5 +1,7 @@ import pyblish.api + import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): @@ -25,14 +27,21 @@ class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): self.log.info("No workfile present for textures") return - msg = "Not matching version: texture v{:03d} - workfile {}" - assert version_str in wfile, \ + if version_str not in wfile: + msg = "Not matching version: texture v{:03d} - workfile {}" msg.format( instance.data["version"], wfile ) + raise PublishXmlValidationError(self, msg) present_versions = set() for instance in instance.context: present_versions.add(instance.data["version"]) - assert len(present_versions) == 1, "Too many versions in a batch!" + if len(present_versions) != 1: + msg = "Too many versions in a batch!" + found = ','.join(["'{}'".format(val) for val in present_versions]) + formatting_data = {"found": found} + + raise PublishXmlValidationError(self, msg, key="too_many", + formatting_data=formatting_data) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py index 25bb5aea4a..fa492a80d8 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -1,11 +1,13 @@ import pyblish.api + import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): """Validates that textures workfile has collected resources (optional). - Collected recourses means secondary workfiles (in most cases). + Collected resources means secondary workfiles (in most cases). """ label = "Validate Texture Workfile Has Resources" @@ -24,6 +26,13 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): self.log.warning("Only secondary workfile present!") return - msg = "No secondary workfiles present for workfile {}".\ - format(instance.data["name"]) - assert instance.data.get("resources"), msg + if not instance.data.get("resources"): + msg = "No secondary workfile present for workfile '{}'". \ + format(instance.data["name"]) + ext = self.main_workfile_extensions[0] + formatting_data = {"file_name": instance.data["name"], + "extension": ext} + + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data + ) diff --git a/openpype/hosts/testhost/plugins/publish/collect_context.py b/openpype/hosts/testhost/plugins/publish/collect_context.py index bbb8477cdf..0ab98fb84b 100644 --- a/openpype/hosts/testhost/plugins/publish/collect_context.py +++ b/openpype/hosts/testhost/plugins/publish/collect_context.py @@ -19,7 +19,7 @@ class CollectContextDataTestHost( hosts = ["testhost"] @classmethod - def get_instance_attr_defs(cls): + def get_attribute_defs(cls): return [ attribute_definitions.BoolDef( "test_bool", diff --git a/openpype/hosts/testhost/plugins/publish/collect_instance_1.py b/openpype/hosts/testhost/plugins/publish/collect_instance_1.py index 979ab83f11..3c035eccb6 100644 --- a/openpype/hosts/testhost/plugins/publish/collect_instance_1.py +++ b/openpype/hosts/testhost/plugins/publish/collect_instance_1.py @@ -20,7 +20,7 @@ class CollectInstanceOneTestHost( hosts = ["testhost"] @classmethod - def get_instance_attr_defs(cls): + def get_attribute_defs(cls): return [ attribute_definitions.NumberDef( "version", diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 381c2c62f0..836a0b4ed1 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -15,6 +15,7 @@ from avalon.pipeline import AVALON_CONTAINER_ID from openpype.hosts import tvpaint from openpype.api import get_current_project_settings from openpype.lib import register_event_callback +from openpype.pipeline import LegacyCreator from .lib import ( execute_george, @@ -77,7 +78,7 @@ def install(): pyblish.api.register_host("tvpaint") pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) registered_callbacks = ( pyblish.api.registered_callbacks().get("instanceToggled") or [] @@ -99,7 +100,7 @@ def uninstall(): pyblish.api.deregister_host("tvpaint") pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def containerise( diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index af80c9eae2..8510794f06 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -3,14 +3,14 @@ import uuid import avalon.api -from openpype.api import PypeCreatorMixin +from openpype.pipeline import LegacyCreator from openpype.hosts.tvpaint.api import ( pipeline, lib ) -class Creator(PypeCreatorMixin, avalon.api.Creator): +class Creator(LegacyCreator): def __init__(self, *args, **kwargs): super(Creator, self).__init__(*args, **kwargs) # Add unified identifier created with `uuid` module diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index 40a7d15990..c1af9632b1 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -1,5 +1,4 @@ -from avalon.api import CreatorError - +from openpype.pipeline import CreatorError from openpype.lib import prepare_template_data from openpype.hosts.tvpaint.api import ( plugin, diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py index af962052fc..a7f717ccec 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py @@ -1,4 +1,4 @@ -from avalon.api import CreatorError +from openpype.pipeline import CreatorError from openpype.lib import prepare_template_data from openpype.hosts.tvpaint.api import ( plugin, diff --git a/openpype/hosts/tvpaint/plugins/load/load_image.py b/openpype/hosts/tvpaint/plugins/load/load_image.py index 7dba1e3619..f861d0119e 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_image.py @@ -1,4 +1,4 @@ -from avalon.vendor import qargparse +import qargparse from openpype.hosts.tvpaint.api import lib, plugin diff --git a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py index 0a85e5dc76..5e4e3965d2 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/openpype/hosts/tvpaint/plugins/load/load_reference_image.py @@ -1,6 +1,6 @@ import collections +import qargparse from avalon.pipeline import get_representation_context -from avalon.vendor import qargparse from openpype.hosts.tvpaint.api import lib, pipeline, plugin diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml new file mode 100644 index 0000000000..33a9ca4247 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml @@ -0,0 +1,22 @@ + + + +Subset context +## Invalid subset context + +Context of the given subset doesn't match your current scene. + +### How to repair? + +Yout can fix this with "Repair" button on the right. This will use '{expected_asset}' asset name and overwrite '{found_asset}' asset name in scene metadata. + +After that restart publishing with Reload button. + + +### How could this happen? + +The subset was created in different scene with different context +or the scene file was copy pasted from different context. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_duplicated_layer_names.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_duplicated_layer_names.xml new file mode 100644 index 0000000000..5d798544c0 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_duplicated_layer_names.xml @@ -0,0 +1,22 @@ + + + +Layer names +## Duplicated layer names + +Can't determine which layers should be published because there are duplicated layer names in the scene. + +### Duplicated layer names + +{layer_names} + +*Check layer names for all subsets in list on left side.* + +### How to repair? + +Hide/rename/remove layers that should not be published. + +If all of them should be published then you have duplicated subset names in the scene. In that case you have to recrete them and use different variant name. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml new file mode 100644 index 0000000000..e7be735888 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_layers_visibility.xml @@ -0,0 +1,20 @@ + + + +Layers visiblity +## All layers are not visible + +Layers visibility was changed during publishing which caused that all layers for subset "{instance_name}" are hidden. + +### Layer names for **{instance_name}** + +{layer_names} + +*Check layer names for all subsets in the list on the left side.* + +### How to repair? + +Reset publishing and do not change visibility of layers after hitting publish button. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_marks.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_marks.xml new file mode 100644 index 0000000000..f0e01ebaa7 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_marks.xml @@ -0,0 +1,21 @@ + + + +Frame range +## Invalid render frame range + +Scene frame range which will be rendered is defined by MarkIn and MarkOut. Expected frame range is {expected_frame_range} and current frame range is {current_frame_range}. + +It is also required that MarkIn and MarkOut are enabled in the scene. Their color is highlighted on timeline when are enabled. + +- MarkIn is {mark_in_enable_state} +- MarkOut is {mark_out_enable_state} + +### How to repair? + +Yout can fix this with "Repair" button on the right. That will change MarkOut to {expected_mark_out}. + +Or you can manually modify MarkIn and MarkOut in the scene timeline. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_missing_layer_names.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_missing_layer_names.xml new file mode 100644 index 0000000000..e96e7c5044 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_missing_layer_names.xml @@ -0,0 +1,18 @@ + + + +Missing layers +## Missing layers for render pass + +Render pass subset "{instance_name}" has stored layer names that belong to it's rendering scope but layers were not found in scene. + +### Missing layer names + +{layer_names} + +### How to repair? + +Find layers that belong to subset {instance_name} and rename them back to expected layer names or remove the subset and create new with right layers. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_render_pass_group.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_render_pass_group.xml new file mode 100644 index 0000000000..df7bdf36e5 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_render_pass_group.xml @@ -0,0 +1,14 @@ + + + +Render pass group +## Invalid group of Render Pass layers + +Layers of Render Pass {instance_name} belong to Render Group which is defined by TVPaint color group {expected_group}. But the layers are not in the group. + +### How to repair? + +Change the color group to {expected_group} on layers {layer_names}. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_scene_settings.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_scene_settings.xml new file mode 100644 index 0000000000..f741c71456 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_scene_settings.xml @@ -0,0 +1,26 @@ + + + +Scene settings +## Invalid scene settings + +Scene settings do not match to expected values. + +**FPS** +- Expected value: {expected_fps} +- Current value: {current_fps} + +**Resolution** +- Expected value: {expected_width}x{expected_height} +- Current value: {current_width}x{current_height} + +**Pixel ratio** +- Expected value: {expected_pixel_ratio} +- Current value: {current_pixel_ratio} + +### How to repair? + +FPS and Pixel ratio can be modified in scene setting. Wrong resolution can be fixed with changing resolution of scene but due to TVPaint limitations it is possible that you will need to create new scene. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_start_frame.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_start_frame.xml new file mode 100644 index 0000000000..9052abf66c --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_start_frame.xml @@ -0,0 +1,14 @@ + + + +First frame +## MarkIn is not set to 0 + +MarkIn in your scene must start from 0 fram index but MarkIn is set to {current_start_frame}. + +### How to repair? + +You can modify MarkIn manually or hit the "Repair" button on the right which will change MarkIn to 0 (does not change MarkOut). + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml new file mode 100644 index 0000000000..7397f6ef0b --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_metadata.xml @@ -0,0 +1,19 @@ + + + +Missing metadata +## Your scene miss context metadata + +Your scene does not contain metadata about {missing_metadata}. + +### How to repair? + +Resave the scene using Workfiles tool or hit the "Repair" button on the right. + + +### How this could happend? + +You're using scene file that was not created using Workfiles tool. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml new file mode 100644 index 0000000000..c4ffafc8b5 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_workfile_project_name.xml @@ -0,0 +1,24 @@ + + + +Project name +## Your scene is from different project + +It is not possible to publish into project "{workfile_project_name}" when TVPaint was opened with project "{env_project_name}" in context. + +### How to repair? + +If the workfile belongs to project "{env_project_name}" then use Workfiles tool to resave it. + +Otherwise close TVPaint and launch it again from project you want to publish in. + + +### How this could happend? + +You've opened workfile from different project. You've opened TVPaint on a task from "{env_project_name}" then you've opened TVPaint again on task from "{workfile_project_name}" without closing the TVPaint. Because TVPaint can run only once the project didn't change. + +### Why it is important? +Because project may affect how TVPaint works or change publishing behavior it is dangerous to allow change project context in many ways. For example publishing will not run as expected. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py index 0fdeba0a21..70816f9f18 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishXmlValidationError from openpype.hosts.tvpaint.api import pipeline @@ -27,7 +28,7 @@ class FixAssetNames(pyblish.api.Action): pipeline._write_instances(new_instance_items) -class ValidateMissingLayers(pyblish.api.ContextPlugin): +class ValidateAssetNames(pyblish.api.ContextPlugin): """Validate assset name present on instance. Asset name on instance should be the same as context's. @@ -48,8 +49,18 @@ class ValidateMissingLayers(pyblish.api.ContextPlugin): instance_label = ( instance.data.get("label") or instance.data["name"] ) - raise AssertionError(( - "Different asset name on instance then context's." - " Instance \"{}\" has asset name: \"{}\"" - " Context asset name is: \"{}\"" - ).format(instance_label, asset_name, context_asset_name)) + + raise PublishXmlValidationError( + self, + ( + "Different asset name on instance then context's." + " Instance \"{}\" has asset name: \"{}\"" + " Context asset name is: \"{}\"" + ).format( + instance_label, asset_name, context_asset_name + ), + formatting_data={ + "expected_asset": context_asset_name, + "found_asset": asset_name + } + ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py b/openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py index efccf19ef9..9f61bdbcd0 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishXmlValidationError class ValidateLayersGroup(pyblish.api.InstancePlugin): @@ -30,14 +31,20 @@ class ValidateLayersGroup(pyblish.api.InstancePlugin): "\"{}\"".format(layer_name) for layer_name in duplicated_layer_names ]) - - # Raise an error - raise AssertionError( + detail_lines = [ + "- {}".format(layer_name) + for layer_name in set(duplicated_layer_names) + ] + raise PublishXmlValidationError( + self, ( "Layers have duplicated names for instance {}." # Description what's wrong " There are layers with same name and one of them is marked" " for publishing so it is not possible to know which should" " be published. Please look for layers with names: {}" - ).format(instance.data["label"], layers_msg) + ).format(instance.data["label"], layers_msg), + formatting_data={ + "layer_names": "
".join(detail_lines) + } ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py index 74ef34169e..7ea0587b8f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py @@ -1,6 +1,8 @@ import pyblish.api +from openpype.pipeline import PublishXmlValidationError +# TODO @iLLiCiTiT add repair action to disable instances? class ValidateLayersVisiblity(pyblish.api.InstancePlugin): """Validate existence of renderPass layers.""" @@ -9,8 +11,26 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin): families = ["review", "renderPass", "renderLayer"] def process(self, instance): + layer_names = set() for layer in instance.data["layers"]: + layer_names.add(layer["name"]) if layer["visible"]: return - raise AssertionError("All layers of instance are not visible.") + instance_label = ( + instance.data.get("label") or instance.data["name"] + ) + + raise PublishXmlValidationError( + self, + "All layers of instance \"{}\" are not visible.".format( + instance_label + ), + formatting_data={ + "instance_name": instance_label, + "layer_names": "
".join([ + "- {}".format(layer_name) + for layer_name in layer_names + ]) + } + ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py index f45247ceac..d1f299e006 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py @@ -1,6 +1,7 @@ import json import pyblish.api +from openpype.pipeline import PublishXmlValidationError from openpype.hosts.tvpaint.api import lib @@ -73,9 +74,34 @@ class ValidateMarks(pyblish.api.ContextPlugin): "expected": expected_data[k] } - if invalid: - raise AssertionError( - "Marks does not match database:\n{}".format( - json.dumps(invalid, sort_keys=True, indent=4) - ) - ) + # Validation ends + if not invalid: + return + + current_frame_range = ( + (current_data["markOut"] - current_data["markIn"]) + 1 + ) + expected_frame_range = ( + (expected_data["markOut"] - expected_data["markIn"]) + 1 + ) + mark_in_enable_state = "disabled" + if current_data["markInState"]: + mark_in_enable_state = "enabled" + + mark_out_enable_state = "disabled" + if current_data["markOutState"]: + mark_out_enable_state = "enabled" + + raise PublishXmlValidationError( + self, + "Marks does not match database:\n{}".format( + json.dumps(invalid, sort_keys=True, indent=4) + ), + formatting_data={ + "current_frame_range": str(current_frame_range), + "expected_frame_range": str(expected_frame_range), + "mark_in_enable_state": mark_in_enable_state, + "mark_out_enable_state": mark_out_enable_state, + "expected_mark_out": expected_data["markOut"] + } + ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_missing_layer_names.py b/openpype/hosts/tvpaint/plugins/publish/validate_missing_layer_names.py index db9d354fcd..294ce6cf4f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_missing_layer_names.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_missing_layer_names.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishXmlValidationError class ValidateMissingLayers(pyblish.api.InstancePlugin): @@ -30,13 +31,25 @@ class ValidateMissingLayers(pyblish.api.InstancePlugin): "\"{}\"".format(layer_name) for layer_name in missing_layer_names ]) + instance_label = ( + instance.data.get("label") or instance.data["name"] + ) + description_layer_names = "
".join([ + "- {}".format(layer_name) + for layer_name in missing_layer_names + ]) # Raise an error - raise AssertionError( + raise PublishXmlValidationError( + self, ( "Layers were not found by name for instance \"{}\"." # Description what's wrong " Layer names marked for publishing are not available" " in layers list. Missing layer names: {}" - ).format(instance.data["label"], layers_msg) + ).format(instance.data["label"], layers_msg), + formatting_data={ + "instance_name": instance_label, + "layer_names": description_layer_names + } ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_project_settings.py b/openpype/hosts/tvpaint/plugins/publish/validate_project_settings.py deleted file mode 100644 index 84c03a9857..0000000000 --- a/openpype/hosts/tvpaint/plugins/publish/validate_project_settings.py +++ /dev/null @@ -1,34 +0,0 @@ -import json - -import pyblish.api - - -class ValidateProjectSettings(pyblish.api.ContextPlugin): - """Validate project settings against database. - """ - - label = "Validate Project Settings" - order = pyblish.api.ValidatorOrder - optional = True - - def process(self, context): - scene_data = { - "fps": context.data.get("sceneFps"), - "resolutionWidth": context.data.get("sceneWidth"), - "resolutionHeight": context.data.get("sceneHeight"), - "pixelAspect": context.data.get("scenePixelAspect") - } - invalid = {} - for k in scene_data.keys(): - expected_value = context.data["assetEntity"]["data"][k] - if scene_data[k] != expected_value: - invalid[k] = { - "current": scene_data[k], "expected": expected_value - } - - if invalid: - raise AssertionError( - "Project settings does not match database:\n{}".format( - json.dumps(invalid, sort_keys=True, indent=4) - ) - ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py b/openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py index 5047b8d729..0fbfca6c56 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py @@ -1,5 +1,6 @@ import collections import pyblish.api +from openpype.pipeline import PublishXmlValidationError class ValidateLayersGroup(pyblish.api.InstancePlugin): @@ -26,11 +27,13 @@ class ValidateLayersGroup(pyblish.api.InstancePlugin): layer_names = instance.data["layer_names"] # Check if all layers from render pass are in right group invalid_layers_by_group_id = collections.defaultdict(list) + invalid_layer_names = set() for layer_name in layer_names: layer = layers_by_name.get(layer_name) _group_id = layer["group_id"] if _group_id != group_id: invalid_layers_by_group_id[_group_id].append(layer) + invalid_layer_names.add(layer_name) # Everything is OK and skip exception if not invalid_layers_by_group_id: @@ -61,16 +64,27 @@ class ValidateLayersGroup(pyblish.api.InstancePlugin): ) # Raise an error - raise AssertionError(( - # Short message - "Layers in wrong group." - # Description what's wrong - " Layers from render pass \"{}\" must be in group {} (id: {})." - # Detailed message - " Layers in wrong group: {}" - ).format( - instance.data["label"], - correct_group["name"], - correct_group["group_id"], - " | ".join(per_group_msgs) - )) + raise PublishXmlValidationError( + self, + ( + # Short message + "Layers in wrong group." + # Description what's wrong + " Layers from render pass \"{}\" must be in group {} (id: {})." + # Detailed message + " Layers in wrong group: {}" + ).format( + instance.data["label"], + correct_group["name"], + correct_group["group_id"], + " | ".join(per_group_msgs) + ), + formatting_data={ + "instance_name": ( + instance.data.get("label") or instance.data["name"] + ), + "expected_group": correct_group["name"], + "layer_names": ", ".join(invalid_layer_names) + + } + ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py b/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py new file mode 100644 index 0000000000..d235215ac9 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py @@ -0,0 +1,49 @@ +import json + +import pyblish.api +from openpype.pipeline import PublishXmlValidationError + + +# TODO @iLliCiTiT add fix action for fps +class ValidateProjectSettings(pyblish.api.ContextPlugin): + """Validate scene settings against database.""" + + label = "Validate Scene Settings" + order = pyblish.api.ValidatorOrder + optional = True + + def process(self, context): + expected_data = context.data["assetEntity"]["data"] + scene_data = { + "fps": context.data.get("sceneFps"), + "resolutionWidth": context.data.get("sceneWidth"), + "resolutionHeight": context.data.get("sceneHeight"), + "pixelAspect": context.data.get("scenePixelAspect") + } + invalid = {} + for k in scene_data.keys(): + expected_value = expected_data[k] + if scene_data[k] != expected_value: + invalid[k] = { + "current": scene_data[k], "expected": expected_value + } + + if not invalid: + return + + raise PublishXmlValidationError( + self, + "Scene settings does not match database:\n{}".format( + json.dumps(invalid, sort_keys=True, indent=4) + ), + formatting_data={ + "expected_fps": expected_data["fps"], + "current_fps": scene_data["fps"], + "expected_width": expected_data["resolutionWidth"], + "expected_height": expected_data["resolutionHeight"], + "current_width": scene_data["resolutionWidth"], + "current_height": scene_data["resolutionWidth"], + "expected_pixel_ratio": expected_data["pixelAspect"], + "current_pixel_ratio": scene_data["pixelAspect"] + } + ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py index e2f8386757..ddc738c6ed 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishXmlValidationError from openpype.hosts.tvpaint.api import lib @@ -24,4 +25,13 @@ class ValidateStartFrame(pyblish.api.ContextPlugin): def process(self, context): start_frame = lib.execute_george("tv_startframe") - assert int(start_frame) == 0, "Start frame has to be frame 0." + if start_frame == 0: + return + + raise PublishXmlValidationError( + self, + "Start frame has to be frame 0.", + formatting_data={ + "current_start_frame": start_frame + } + ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py index 48fbeedb59..eac345f395 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishXmlValidationError from openpype.hosts.tvpaint.api import save_file @@ -42,8 +43,12 @@ class ValidateWorkfileMetadata(pyblish.api.ContextPlugin): missing_keys.append(key) if missing_keys: - raise AssertionError( + raise PublishXmlValidationError( + self, "Current workfile is missing metadata about {}.".format( ", ".join(missing_keys) - ) + ), + formatting_data={ + "missing_metadata": ", ".join(missing_keys) + } ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py index cc664d8030..0f25f2f7be 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py @@ -1,5 +1,6 @@ import os import pyblish.api +from openpype.pipeline import PublishXmlValidationError class ValidateWorkfileProjectName(pyblish.api.ContextPlugin): @@ -31,15 +32,23 @@ class ValidateWorkfileProjectName(pyblish.api.ContextPlugin): return # Raise an error - raise AssertionError(( - # Short message - "Workfile from different Project ({})." - # Description what's wrong - " It is not possible to publish when TVPaint was launched in" - "context of different project. Current context project is \"{}\"." - " Launch TVPaint in context of project \"{}\" and then publish." - ).format( - workfile_project_name, - env_project_name, - workfile_project_name, - )) + raise PublishXmlValidationError( + self, + ( + # Short message + "Workfile from different Project ({})." + # Description what's wrong + " It is not possible to publish when TVPaint was launched in" + "context of different project. Current context project is" + " \"{}\". Launch TVPaint in context of project \"{}\"" + " and then publish." + ).format( + workfile_project_name, + env_project_name, + workfile_project_name, + ), + formatting_data={ + "workfile_project_name": workfile_project_name, + "expected_project_name": env_project_name + } + ) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index ad64d56e9e..8ab19bd697 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -7,6 +7,7 @@ import pyblish.api from avalon.pipeline import AVALON_CONTAINER_ID from avalon import api +from openpype.pipeline import LegacyCreator from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -44,7 +45,7 @@ def install(): logger.info("installing OpenPype for Unreal") pyblish.api.register_plugin_path(str(PUBLISH_PATH)) api.register_plugin_path(api.Loader, str(LOAD_PATH)) - api.register_plugin_path(api.Creator, str(CREATE_PATH)) + api.register_plugin_path(LegacyCreator, str(CREATE_PATH)) _register_callbacks() _register_events() @@ -53,7 +54,7 @@ def uninstall(): """Uninstall Unreal configuration for Avalon.""" pyblish.api.deregister_plugin_path(str(PUBLISH_PATH)) api.deregister_plugin_path(api.Loader, str(LOAD_PATH)) - api.deregister_plugin_path(api.Creator, str(CREATE_PATH)) + api.deregister_plugin_path(LegacyCreator, str(CREATE_PATH)) def _register_callbacks(): @@ -70,7 +71,7 @@ def _register_events(): pass -class Creator(api.Creator): +class Creator(LegacyCreator): hosts = ["unreal"] asset_types = [] diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 2327fc09c8..dd2e7750f0 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from abc import ABC -import openpype.api +from openpype.pipeline import LegacyCreator import avalon.api -class Creator(openpype.api.Creator): +class Creator(LegacyCreator): """This serves as skeleton for future OpenPype specific functionality""" defaults = ['Main'] diff --git a/openpype/hosts/webpublisher/api/__init__.py b/openpype/hosts/webpublisher/api/__init__.py index f338c92a5e..a0eaef03ef 100644 --- a/openpype/hosts/webpublisher/api/__init__.py +++ b/openpype/hosts/webpublisher/api/__init__.py @@ -5,6 +5,7 @@ from avalon import api as avalon from avalon import io from pyblish import api as pyblish import openpype.hosts.webpublisher +from openpype.pipeline import LegacyCreator log = logging.getLogger("openpype.hosts.webpublisher") @@ -21,7 +22,7 @@ def install(): pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + avalon.register_plugin_path(LegacyCreator, CREATE_PATH) log.info(PUBLISH_PATH) io.install() @@ -30,7 +31,7 @@ def install(): def uninstall(): pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) - avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH) + avalon.deregister_plugin_path(LegacyCreator, CREATE_PATH) # to have required methods for interface diff --git a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py index abad14106f..8b21842635 100644 --- a/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py +++ b/openpype/hosts/webpublisher/plugins/publish/collect_published_files.py @@ -10,14 +10,18 @@ Provides: import os import clique import tempfile +import math + from avalon import io import pyblish.api -from openpype.lib import prepare_template_data +from openpype.lib import prepare_template_data, get_asset, ffprobe_streams +from openpype.lib.vendor_bin_utils import get_fps from openpype.lib.plugin_tools import ( parse_json, get_subset_name_with_asset_doc ) + class CollectPublishedFiles(pyblish.api.ContextPlugin): """ This collector will try to find json files in provided @@ -49,10 +53,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): self.log.info("task_sub:: {}".format(task_subfolders)) asset_name = context.data["asset"] - asset_doc = io.find_one({ - "type": "asset", - "name": asset_name - }) + asset_doc = get_asset() task_name = context.data["task"] task_type = context.data["taskType"] project_name = context.data["project_name"] @@ -97,11 +98,26 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): instance.data["frameEnd"] = \ instance.data["representations"][0]["frameEnd"] else: - instance.data["frameStart"] = 0 - instance.data["frameEnd"] = 1 + frame_start = asset_doc["data"]["frameStart"] + instance.data["frameStart"] = frame_start + instance.data["frameEnd"] = asset_doc["data"]["frameEnd"] instance.data["representations"] = self._get_single_repre( task_dir, task_data["files"], tags ) + file_url = os.path.join(task_dir, task_data["files"][0]) + duration = self._get_duration(file_url) + if duration: + try: + frame_end = int(frame_start) + math.ceil(duration) + instance.data["frameEnd"] = math.ceil(frame_end) + self.log.debug("frameEnd:: {}".format( + instance.data["frameEnd"])) + except ValueError: + self.log.warning("Unable to count frames " + "duration {}".format(duration)) + + instance.data["handleStart"] = asset_doc["data"]["handleStart"] + instance.data["handleEnd"] = asset_doc["data"]["handleEnd"] self.log.info("instance.data:: {}".format(instance.data)) @@ -127,7 +143,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): return [repre_data] def _process_sequence(self, files, task_dir, tags): - """Prepare reprentations for sequence of files.""" + """Prepare representation for sequence of files.""" collections, remainder = clique.assemble(files) assert len(collections) == 1, \ "Too many collections in {}".format(files) @@ -188,6 +204,7 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): msg = "No family found for combination of " +\ "task_type: {}, is_sequence:{}, extension: {}".format( task_type, is_sequence, extension) + found_family = "render" assert found_family, msg return (found_family, @@ -243,3 +260,41 @@ class CollectPublishedFiles(pyblish.api.ContextPlugin): return version[0].get("version") or 0 else: return 0 + + def _get_duration(self, file_url): + """Return duration in frames""" + try: + streams = ffprobe_streams(file_url, self.log) + except Exception as exc: + raise AssertionError(( + "FFprobe couldn't read information about input file: \"{}\"." + " Error message: {}" + ).format(file_url, str(exc))) + + first_video_stream = None + for stream in streams: + if "width" in stream and "height" in stream: + first_video_stream = stream + break + + if first_video_stream: + nb_frames = stream.get("nb_frames") + if nb_frames: + try: + return int(nb_frames) + except ValueError: + self.log.warning( + "nb_frames {} not convertible".format(nb_frames)) + + duration = stream.get("duration") + frame_rate = get_fps(stream.get("r_frame_rate", '0/0')) + self.log.debu("duration:: {} frame_rate:: {}".format( + duration, frame_rate)) + try: + return float(duration) * float(frame_rate) + except ValueError: + self.log.warning( + "{} or {} cannot be converted".format(duration, + frame_rate)) + + self.log.warning("Cannot get number of frames") diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 59ffa22462..3c62d47daf 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -165,6 +165,7 @@ from .plugin_tools import ( ) from .path_tools import ( + create_hard_link, version_up, get_version_from_path, get_last_version_from_path, @@ -300,6 +301,7 @@ __all__ = [ "get_unique_layer_name", "get_background_layers", + "create_hard_link", "version_up", "get_version_from_path", "get_last_version_from_path", diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py index a61603fa05..9fc65aae8e 100644 --- a/openpype/lib/delivery.py +++ b/openpype/lib/delivery.py @@ -71,15 +71,14 @@ def path_from_representation(representation, anatomy): def copy_file(src_path, dst_path): """Hardlink file if possible(to save space), copy if not""" - from avalon.vendor import filelink # safer importing + from openpype.lib import create_hard_link # safer importing if os.path.exists(dst_path): return try: - filelink.create( + create_hard_link( src_path, - dst_path, - filelink.HARDLINK + dst_path ) except OSError: shutil.copyfile(src_path, dst_path) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index d6c32ad9e8..3a9f835272 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -4,9 +4,9 @@ import abc import json import logging import six +import platform from openpype.settings import get_project_settings -from openpype.settings.lib import get_site_local_overrides from .anatomy import Anatomy from .profiles_filtering import filter_profiles @@ -14,6 +14,41 @@ from .profiles_filtering import filter_profiles log = logging.getLogger(__name__) +def create_hard_link(src_path, dst_path): + """Create hardlink of file. + + Args: + src_path(str): Full path to a file which is used as source for + hardlink. + dst_path(str): Full path to a file where a link of source will be + added. + """ + # Use `os.link` if is available + # - should be for all platforms with newer python versions + if hasattr(os, "link"): + os.link(src_path, dst_path) + return + + # Windows implementation of hardlinks + # - used in Python 2 + if platform.system().lower() == "windows": + import ctypes + from ctypes.wintypes import BOOL + CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW + CreateHardLink.argtypes = [ + ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p + ] + CreateHardLink.restype = BOOL + + res = CreateHardLink(dst_path, src_path, None) + if res == 0: + raise ctypes.WinError() + # Raises not implemented error if gets here + raise NotImplementedError( + "Implementation of hardlink for current environment is missing." + ) + + def _rreplace(s, a, b, n=1): """Replace a with b in string s from right side n times.""" return b.join(s.rsplit(a, n)) diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 183aad939a..19765a6f4a 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -293,7 +293,7 @@ def set_plugin_attributes_from_settings( plugin_type = None if superclass.__name__.split(".")[-1] in ("Loader", "SubsetLoader"): plugin_type = "load" - elif superclass.__name__.split(".")[-1] == "Creator": + elif superclass.__name__.split(".")[-1] in ("Creator", "LegacyCreator"): plugin_type = "create" if not host_name or not project_name or plugin_type is None: diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 4be016f656..4b11f1c046 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -204,3 +204,23 @@ def is_oiio_supported(): )) return False return True + + +def get_fps(str_value): + """Returns (str) value of fps from ffprobe frame format (120/1)""" + if str_value == "0/0": + print("WARNING: Source has \"r_frame_rate\" value set to \"0/0\".") + return "Unknown" + + items = str_value.split("/") + if len(items) == 1: + fps = float(items[0]) + + elif len(items) == 2: + fps = float(items[0]) / float(items[1]) + + # Check if fps is integer or float number + if int(fps) == fps: + fps = int(fps) + + return str(fps) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index c7078475df..175957ae39 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -61,6 +61,7 @@ class _ModuleClass(object): def __init__(self, name): # Call setattr on super class super(_ModuleClass, self).__setattr__("name", name) + super(_ModuleClass, self).__setattr__("__name__", name) # Where modules and interfaces are stored super(_ModuleClass, self).__setattr__("__attributes__", dict()) @@ -72,7 +73,7 @@ class _ModuleClass(object): if attr_name not in self.__attributes__: if attr_name in ("__path__", "__file__"): return None - raise ImportError("No module named {}.{}".format( + raise AttributeError("'{}' has not attribute '{}'".format( self.name, attr_name )) return self.__attributes__[attr_name] diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c7a14791e4..1de1c37575 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -516,7 +516,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ representations = [] collections, remainders = clique.assemble(exp_files) - bake_renders = instance.get("bakingNukeScripts", []) # create representation for every collected sequento ce for collection in collections: @@ -534,9 +533,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): preview = True break - if bake_renders: - preview = False - # toggle preview on if multipart is on if instance.get("multipartExr", False): preview = True @@ -610,16 +606,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): }) self._solve_families(instance, True) - if (bake_renders - and remainder in bake_renders[0]["bakeRenderPath"]): - rep.update({ - "fps": instance.get("fps"), - "tags": ["review", "delete"] - }) - # solve families with `preview` attributes - self._solve_families(instance, True) - representations.append(rep) - return representations def _solve_families(self, instance, preview=False): diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index d49e314179..c2426e0d78 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -107,6 +107,10 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): explicitly and manually changed the frame list on the Deadline job. """ + # no frames in file name at all, eg 'renderCompositingMain.withLut.mov' + if not frame_placeholder: + return set([file_name_template]) + real_expected_rendered = set() src_padding_exp = "%0{}d".format(len(frame_placeholder)) for frames in frame_list: @@ -130,14 +134,13 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): # There might be cases where clique was unable to collect # collections in `collect_frames` - thus we capture that case - if frame is None: - self.log.warning("Unable to detect frame from filename: " - "{}".format(file_name)) - continue + if frame is not None: + frame_placeholder = "#" * len(frame) - frame_placeholder = "#" * len(frame) - file_name_template = os.path.basename( - file_name.replace(frame, frame_placeholder)) + file_name_template = os.path.basename( + file_name.replace(frame, frame_placeholder)) + else: + file_name_template = file_name break return file_name_template, frame_placeholder diff --git a/openpype/modules/ftrack/plugins/publish/collect_username.py b/openpype/modules/ftrack/plugins/publish/collect_username.py index 84d7f60a3f..a9b746ea51 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_username.py +++ b/openpype/modules/ftrack/plugins/publish/collect_username.py @@ -23,8 +23,11 @@ class CollectUsername(pyblish.api.ContextPlugin): Expects "pype.club" user created on Ftrack and FTRACK_BOT_API_KEY env var set up. + Resets `context.data["user"] to correctly populate `version.author` and + `representation.context.username` + """ - order = pyblish.api.CollectorOrder - 0.488 + order = pyblish.api.CollectorOrder + 0.0015 label = "Collect ftrack username" hosts = ["webpublisher", "photoshop"] targets = ["remotepublish", "filespublish", "tvpaint_worker"] @@ -65,3 +68,4 @@ class CollectUsername(pyblish.api.ContextPlugin): if '@' in burnin_name: burnin_name = burnin_name[:burnin_name.index('@')] os.environ["WEBPUBLISH_OPENPYPE_USERNAME"] = burnin_name + context.data["user"] = burnin_name diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index 5a67780413..ff77405de5 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -1,5 +1,5 @@ from Qt import QtCore, QtWidgets -from avalon.vendor import qtawesome +import qtawesome from .models import LogModel, LogsFilterProxy diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 80f41992cb..7241cc3472 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -4,9 +4,9 @@ from bson.objectid import ObjectId from Qt import QtCore from Qt.QtCore import Qt +import qtawesome from openpype.tools.utils.delegates import pretty_timestamp -from avalon.vendor import qtawesome from openpype.lib import PypeLogger from openpype.api import get_local_site_id diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 18487b3d11..6aae9562cf 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -5,6 +5,7 @@ from functools import partial from Qt import QtWidgets, QtCore, QtGui from Qt.QtCore import Qt +import qtawesome from openpype.tools.settings import style @@ -12,7 +13,6 @@ from openpype.api import get_local_site_id from openpype.lib import PypeLogger from openpype.tools.utils.delegates import pretty_timestamp -from avalon.vendor import qtawesome from .models import ( SyncRepresentationSummaryModel, diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index e968df4011..7147e56dd2 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -4,11 +4,17 @@ from .create import ( BaseCreator, Creator, AutoCreator, - CreatedInstance + CreatedInstance, + + CreatorError, + + LegacyCreator, + legacy_create, ) from .publish import ( PublishValidationError, + PublishXmlValidationError, KnownPublishError, OpenPypePyblishPluginMixin ) @@ -22,7 +28,14 @@ __all__ = ( "AutoCreator", "CreatedInstance", + "CreatorError", + + # Legacy creation + "LegacyCreator", + "legacy_create", + "PublishValidationError", + "PublishXmlValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin" ) diff --git a/openpype/pipeline/create/__init__.py b/openpype/pipeline/create/__init__.py index 948b719851..9571f56b8f 100644 --- a/openpype/pipeline/create/__init__.py +++ b/openpype/pipeline/create/__init__.py @@ -14,6 +14,11 @@ from .context import ( CreateContext ) +from .legacy_create import ( + LegacyCreator, + legacy_create, +) + __all__ = ( "SUBSET_NAME_ALLOWED_SYMBOLS", @@ -25,5 +30,8 @@ __all__ = ( "AutoCreator", "CreatedInstance", - "CreateContext" + "CreateContext", + + "LegacyCreator", + "legacy_create", ) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 706279fd72..c2757a4502 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1005,12 +1005,14 @@ class CreateContext: if not instances: return - task_names_by_asset_name = collections.defaultdict(set) + task_names_by_asset_name = {} for instance in instances: task_name = instance.get("task") asset_name = instance.get("asset") - if asset_name and task_name: - task_names_by_asset_name[asset_name].add(task_name) + if asset_name: + task_names_by_asset_name[asset_name] = set() + if task_name: + task_names_by_asset_name[asset_name].add(task_name) asset_names = [ asset_name diff --git a/openpype/pipeline/create/legacy_create.py b/openpype/pipeline/create/legacy_create.py new file mode 100644 index 0000000000..d05cdff689 --- /dev/null +++ b/openpype/pipeline/create/legacy_create.py @@ -0,0 +1,156 @@ +"""Create workflow moved from avalon-core repository. + +Renamed classes and functions +- 'Creator' -> 'LegacyCreator' +- 'create' -> 'legacy_create' +""" + +import logging +import collections + +from openpype.lib import get_subset_name + + +class LegacyCreator(object): + """Determine how assets are created""" + label = None + family = None + defaults = None + maintain_selection = True + + dynamic_subset_keys = [] + + log = logging.getLogger("LegacyCreator") + + def __init__(self, name, asset, options=None, data=None): + self.name = name # For backwards compatibility + self.options = options + + # Default data + self.data = collections.OrderedDict() + self.data["id"] = "pyblish.avalon.instance" + self.data["family"] = self.family + self.data["asset"] = asset + self.data["subset"] = name + self.data["active"] = True + + self.data.update(data or {}) + + def process(self): + pass + + @classmethod + def get_dynamic_data( + cls, variant, task_name, asset_id, project_name, host_name + ): + """Return dynamic data for current Creator plugin. + + By default return keys from `dynamic_subset_keys` attribute as mapping + to keep formatted template unchanged. + + ``` + dynamic_subset_keys = ["my_key"] + --- + output = { + "my_key": "{my_key}" + } + ``` + + Dynamic keys may override default Creator keys (family, task, asset, + ...) but do it wisely if you need. + + All of keys will be converted into 3 variants unchanged, capitalized + and all upper letters. Because of that are all keys lowered. + + This method can be modified to prefill some values just keep in mind it + is class method. + + Returns: + dict: Fill data for subset name template. + """ + dynamic_data = {} + for key in cls.dynamic_subset_keys: + key = key.lower() + dynamic_data[key] = "{" + key + "}" + return dynamic_data + + @classmethod + def get_subset_name( + cls, variant, task_name, asset_id, project_name, host_name=None + ): + """Return subset name created with entered arguments. + + Logic extracted from Creator tool. This method should give ability + to get subset name without the tool. + + TODO: Maybe change `variant` variable. + + By default is output concatenated family with user text. + + Args: + variant (str): What is entered by user in creator tool. + task_name (str): Context's task name. + asset_id (ObjectId): Mongo ID of context's asset. + project_name (str): Context's project name. + host_name (str): Name of host. + + Returns: + str: Formatted subset name with entered arguments. Should match + config's logic. + """ + + dynamic_data = cls.get_dynamic_data( + variant, task_name, asset_id, project_name, host_name + ) + + return get_subset_name( + cls.family, + variant, + task_name, + asset_id, + project_name, + host_name, + dynamic_data=dynamic_data + ) + + +def legacy_create(Creator, name, asset, options=None, data=None): + """Create a new instance + + Associate nodes with a subset and family. These nodes are later + validated, according to their `family`, and integrated into the + shared environment, relative their `subset`. + + Data relative each family, along with default data, are imprinted + into the resulting objectSet. This data is later used by extractors + and finally asset browsers to help identify the origin of the asset. + + Arguments: + Creator (Creator): Class of creator + name (str): Name of subset + asset (str): Name of asset + options (dict, optional): Additional options from GUI + data (dict, optional): Additional data from GUI + + Raises: + NameError on `subset` already exists + KeyError on invalid dynamic property + RuntimeError on host error + + Returns: + Name of instance + + """ + from avalon.api import registered_host + host = registered_host() + plugin = Creator(name, asset, options, data) + + if plugin.maintain_selection is True: + with host.maintained_selection(): + print("Running %s with maintained selection" % plugin) + instance = plugin.process() + return instance + + print("Running %s" % plugin) + instance = plugin.process() + return instance diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index ca958816fe..c2729a46ce 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -1,20 +1,26 @@ from .publish_plugins import ( PublishValidationError, + PublishXmlValidationError, KnownPublishError, - OpenPypePyblishPluginMixin + OpenPypePyblishPluginMixin, ) from .lib import ( DiscoverResult, - publish_plugins_discover + publish_plugins_discover, + load_help_content_from_plugin, + load_help_content_from_filepath, ) __all__ = ( "PublishValidationError", + "PublishXmlValidationError", "KnownPublishError", "OpenPypePyblishPluginMixin", "DiscoverResult", - "publish_plugins_discover" + "publish_plugins_discover", + "load_help_content_from_plugin", + "load_help_content_from_filepath", ) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index d3e4ec8a02..739b2c8806 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -1,6 +1,8 @@ import os import sys import types +import inspect +import xml.etree.ElementTree import six import pyblish.plugin @@ -28,6 +30,60 @@ class DiscoverResult: self.plugins[item] = value +class HelpContent: + def __init__(self, title, description, detail=None): + self.title = title + self.description = description + self.detail = detail + + +def load_help_content_from_filepath(filepath): + """Load help content from xml file. + Xml file may containt errors and warnings. + """ + errors = {} + warnings = {} + output = { + "errors": errors, + "warnings": warnings + } + if not os.path.exists(filepath): + return output + tree = xml.etree.ElementTree.parse(filepath) + root = tree.getroot() + for child in root: + child_id = child.attrib.get("id") + if child_id is None: + continue + + # Make sure ID is string + child_id = str(child_id) + + title = child.find("title").text + description = child.find("description").text + detail_node = child.find("detail") + detail = None + if detail_node is not None: + detail = detail_node.text + if child.tag == "error": + errors[child_id] = HelpContent(title, description, detail) + elif child.tag == "warning": + warnings[child_id] = HelpContent(title, description, detail) + return output + + +def load_help_content_from_plugin(plugin): + cls = plugin + if not inspect.isclass(plugin): + cls = plugin.__class__ + plugin_filepath = inspect.getfile(cls) + plugin_dir = os.path.dirname(plugin_filepath) + basename = os.path.splitext(os.path.basename(plugin_filepath))[0] + filename = basename + ".xml" + filepath = os.path.join(plugin_dir, "help", filename) + return load_help_content_from_filepath(filepath) + + def publish_plugins_discover(paths=None): """Find and return available pyblish plug-ins diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index b60b9f43a7..bce64ec709 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -1,3 +1,6 @@ +from .lib import load_help_content_from_plugin + + class PublishValidationError(Exception): """Validation error happened during publishing. @@ -12,13 +15,34 @@ class PublishValidationError(Exception): description(str): Detailed description of an error. It is possible to use Markdown syntax. """ - def __init__(self, message, title=None, description=None): + def __init__(self, message, title=None, description=None, detail=None): self.message = message self.title = title or "< Missing title >" self.description = description or message + self.detail = detail super(PublishValidationError, self).__init__(message) +class PublishXmlValidationError(PublishValidationError): + def __init__( + self, plugin, message, key=None, formatting_data=None + ): + if key is None: + key = "main" + + if not formatting_data: + formatting_data = {} + result = load_help_content_from_plugin(plugin) + content_obj = result["errors"][key] + description = content_obj.description.format(**formatting_data) + detail = content_obj.detail + if detail: + detail = detail.format(**formatting_data) + super(PublishXmlValidationError, self).__init__( + message, content_obj.title, description, detail + ) + + class KnownPublishError(Exception): """Publishing crashed because of known error. diff --git a/openpype/plugin.py b/openpype/plugin.py index 45c9a08209..3569936dac 100644 --- a/openpype/plugin.py +++ b/openpype/plugin.py @@ -3,79 +3,12 @@ import os import pyblish.api import avalon.api -from openpype.lib import get_subset_name - ValidatePipelineOrder = pyblish.api.ValidatorOrder + 0.05 ValidateContentsOrder = pyblish.api.ValidatorOrder + 0.1 ValidateSceneOrder = pyblish.api.ValidatorOrder + 0.2 ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 -class PypeCreatorMixin: - """Helper to override avalon's default class methods. - - Mixin class must be used as first in inheritance order to override methods. - """ - dynamic_subset_keys = [] - - @classmethod - def get_dynamic_data( - cls, variant, task_name, asset_id, project_name, host_name - ): - """Return dynamic data for current Creator plugin. - - By default return keys from `dynamic_subset_keys` attribute as mapping - to keep formatted template unchanged. - - ``` - dynamic_subset_keys = ["my_key"] - --- - output = { - "my_key": "{my_key}" - } - ``` - - Dynamic keys may override default Creator keys (family, task, asset, - ...) but do it wisely if you need. - - All of keys will be converted into 3 variants unchanged, capitalized - and all upper letters. Because of that are all keys lowered. - - This method can be modified to prefill some values just keep in mind it - is class method. - - Returns: - dict: Fill data for subset name template. - """ - dynamic_data = {} - for key in cls.dynamic_subset_keys: - key = key.lower() - dynamic_data[key] = "{" + key + "}" - return dynamic_data - - @classmethod - def get_subset_name( - cls, variant, task_name, asset_id, project_name, host_name=None - ): - dynamic_data = cls.get_dynamic_data( - variant, task_name, asset_id, project_name, host_name - ) - - return get_subset_name( - cls.family, - variant, - task_name, - asset_id, - project_name, - host_name, - dynamic_data=dynamic_data - ) - - -class Creator(PypeCreatorMixin, avalon.api.Creator): - pass - - class ContextPlugin(pyblish.api.ContextPlugin): def process(cls, *args, **kwargs): super(ContextPlugin, cls).process(cls, *args, **kwargs) diff --git a/openpype/plugins/load/delete_old_versions.py b/openpype/plugins/load/delete_old_versions.py index b2f2c88975..e8612745fb 100644 --- a/openpype/plugins/load/delete_old_versions.py +++ b/openpype/plugins/load/delete_old_versions.py @@ -5,10 +5,10 @@ import uuid import clique from pymongo import UpdateOne import ftrack_api +import qargparse from Qt import QtWidgets, QtCore from avalon import api, style -from avalon.vendor import qargparse from avalon.api import AvalonMongoDB import avalon.pipeline from openpype.api import Anatomy diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index b4a5117959..bec1f75425 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -19,7 +19,6 @@ from openpype.lib import ( should_convert_for_ffmpeg, convert_for_ffmpeg, - get_transcode_temp_directory, get_transcode_temp_directory ) import speedcopy @@ -972,16 +971,12 @@ class ExtractReview(pyblish.api.InstancePlugin): def get_letterbox_filters( self, letter_box_def, - input_res_ratio, - output_res_ratio, - pixel_aspect, - scale_factor_by_width, - scale_factor_by_height + output_width, + output_height ): output = [] ratio = letter_box_def["ratio"] - state = letter_box_def["state"] fill_color = letter_box_def["fill_color"] f_red, f_green, f_blue, f_alpha = fill_color fill_color_hex = "{0:0>2X}{1:0>2X}{2:0>2X}".format( @@ -997,75 +992,129 @@ class ExtractReview(pyblish.api.InstancePlugin): ) line_color_alpha = float(l_alpha) / 255 - if input_res_ratio == output_res_ratio: - ratio /= pixel_aspect - elif input_res_ratio < output_res_ratio: - ratio /= scale_factor_by_width - else: - ratio /= scale_factor_by_height + # test ratios and define if pillar or letter boxes + output_ratio = float(output_width) / float(output_height) + self.log.debug("Output ratio: {} LetterBox ratio: {}".format( + output_ratio, ratio + )) + pillar = output_ratio > ratio + need_mask = format(output_ratio, ".3f") != format(ratio, ".3f") + if not need_mask: + return [] - if state == "letterbox": + if not pillar: if fill_color_alpha > 0: top_box = ( - "drawbox=0:0:iw:round((ih-(iw*(1/{})))/2):t=fill:c={}@{}" - ).format(ratio, fill_color_hex, fill_color_alpha) + "drawbox=0:0:{width}" + ":round(({height}-({width}/{ratio}))/2)" + ":t=fill:c={color}@{alpha}" + ).format( + width=output_width, + height=output_height, + ratio=ratio, + color=fill_color_hex, + alpha=fill_color_alpha + ) bottom_box = ( - "drawbox=0:ih-round((ih-(iw*(1/{0})))/2)" - ":iw:round((ih-(iw*(1/{0})))/2):t=fill:c={1}@{2}" - ).format(ratio, fill_color_hex, fill_color_alpha) - + "drawbox=0" + ":{height}-round(({height}-({width}/{ratio}))/2)" + ":{width}" + ":round(({height}-({width}/{ratio}))/2)" + ":t=fill:c={color}@{alpha}" + ).format( + width=output_width, + height=output_height, + ratio=ratio, + color=fill_color_hex, + alpha=fill_color_alpha + ) output.extend([top_box, bottom_box]) if line_color_alpha > 0 and line_thickness > 0: top_line = ( - "drawbox=0:round((ih-(iw*(1/{0})))/2)-{1}:iw:{1}:" - "t=fill:c={2}@{3}" + "drawbox=0" + ":round(({height}-({width}/{ratio}))/2)-{l_thick}" + ":{width}:{l_thick}:t=fill:c={l_color}@{l_alpha}" ).format( - ratio, line_thickness, line_color_hex, line_color_alpha + width=output_width, + height=output_height, + ratio=ratio, + l_thick=line_thickness, + l_color=line_color_hex, + l_alpha=line_color_alpha ) bottom_line = ( - "drawbox=0:ih-round((ih-(iw*(1/{})))/2)" - ":iw:{}:t=fill:c={}@{}" + "drawbox=0" + ":{height}-round(({height}-({width}/{ratio}))/2)" + ":{width}:{l_thick}:t=fill:c={l_color}@{l_alpha}" ).format( - ratio, line_thickness, line_color_hex, line_color_alpha + width=output_width, + height=output_height, + ratio=ratio, + l_thick=line_thickness, + l_color=line_color_hex, + l_alpha=line_color_alpha ) output.extend([top_line, bottom_line]) - elif state == "pillar": + else: if fill_color_alpha > 0: left_box = ( - "drawbox=0:0:round((iw-(ih*{}))/2):ih:t=fill:c={}@{}" - ).format(ratio, fill_color_hex, fill_color_alpha) + "drawbox=0:0" + ":round(({width}-({height}*{ratio}))/2)" + ":{height}" + ":t=fill:c={color}@{alpha}" + ).format( + width=output_width, + height=output_height, + ratio=ratio, + color=fill_color_hex, + alpha=fill_color_alpha + ) right_box = ( - "drawbox=iw-round((iw-(ih*{0}))/2))" - ":0:round((iw-(ih*{0}))/2):ih:t=fill:c={1}@{2}" - ).format(ratio, fill_color_hex, fill_color_alpha) - + "drawbox=" + "{width}-round(({width}-({height}*{ratio}))/2)" + ":0" + ":round(({width}-({height}*{ratio}))/2)" + ":{height}" + ":t=fill:c={color}@{alpha}" + ).format( + width=output_width, + height=output_height, + ratio=ratio, + color=fill_color_hex, + alpha=fill_color_alpha + ) output.extend([left_box, right_box]) if line_color_alpha > 0 and line_thickness > 0: left_line = ( - "drawbox=round((iw-(ih*{}))/2):0:{}:ih:t=fill:c={}@{}" + "drawbox=round(({width}-({height}*{ratio}))/2)" + ":0:{l_thick}:{height}:t=fill:c={l_color}@{l_alpha}" ).format( - ratio, line_thickness, line_color_hex, line_color_alpha + width=output_width, + height=output_height, + ratio=ratio, + l_thick=line_thickness, + l_color=line_color_hex, + l_alpha=line_color_alpha ) right_line = ( - "drawbox=iw-round((iw-(ih*{}))/2))" - ":0:{}:ih:t=fill:c={}@{}" + "drawbox={width}-round(({width}-({height}*{ratio}))/2)" + ":0:{l_thick}:{height}:t=fill:c={l_color}@{l_alpha}" ).format( - ratio, line_thickness, line_color_hex, line_color_alpha + width=output_width, + height=output_height, + ratio=ratio, + l_thick=line_thickness, + l_color=line_color_hex, + l_alpha=line_color_alpha ) - output.extend([left_line, right_line]) - else: - raise ValueError( - "Letterbox state \"{}\" is not recognized".format(state) - ) - return output def rescaling_filters(self, temp_data, output_def, new_repre): @@ -1079,6 +1128,20 @@ class ExtractReview(pyblish.api.InstancePlugin): """ filters = [] + # if reformat input video file is already reforamted from upstream + reformat_in_baking = bool("reformated" in new_repre["tags"]) + self.log.debug("reformat_in_baking: `{}`".format(reformat_in_baking)) + + # Get instance data + pixel_aspect = temp_data["pixel_aspect"] + + if reformat_in_baking: + self.log.debug(( + "Using resolution from input. It is already " + "reformated from upstream process" + )) + pixel_aspect = 1 + # NOTE Skipped using instance's resolution full_input_path_single_file = temp_data["full_input_path_single_file"] try: @@ -1141,12 +1204,6 @@ class ExtractReview(pyblish.api.InstancePlugin): output_width = input_width output_height = input_height - letter_box_def = output_def["letter_box"] - letter_box_enabled = letter_box_def["enabled"] - - # Get instance data - pixel_aspect = temp_data["pixel_aspect"] - # Make sure input width and height is not an odd number input_width_is_odd = bool(input_width % 2 != 0) input_height_is_odd = bool(input_height % 2 != 0) @@ -1171,9 +1228,6 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("input_width: `{}`".format(input_width)) self.log.debug("input_height: `{}`".format(input_height)) - reformat_in_baking = bool("reformated" in new_repre["tags"]) - self.log.debug("reformat_in_baking: `{}`".format(reformat_in_baking)) - # Use instance resolution if output definition has not set it. if output_width is None or output_height is None: output_width = temp_data["resolution_width"] @@ -1185,17 +1239,6 @@ class ExtractReview(pyblish.api.InstancePlugin): output_width = input_width output_height = input_height - if reformat_in_baking: - self.log.debug(( - "Using resolution from input. It is already " - "reformated from baking process" - )) - output_width = input_width - output_height = input_height - pixel_aspect = 1 - new_repre["resolutionWidth"] = input_width - new_repre["resolutionHeight"] = input_height - output_width = int(output_width) output_height = int(output_height) @@ -1219,6 +1262,9 @@ class ExtractReview(pyblish.api.InstancePlugin): "Output resolution is {}x{}".format(output_width, output_height) ) + letter_box_def = output_def["letter_box"] + letter_box_enabled = letter_box_def["enabled"] + # Skip processing if resolution is same as input's and letterbox is # not set if ( @@ -1262,25 +1308,6 @@ class ExtractReview(pyblish.api.InstancePlugin): "scale_factor_by_height: `{}`".format(scale_factor_by_height) ) - # letter_box - if letter_box_enabled: - filters.extend([ - "scale={}x{}:flags=lanczos".format( - output_width, output_height - ), - "setsar=1" - ]) - filters.extend( - self.get_letterbox_filters( - letter_box_def, - input_res_ratio, - output_res_ratio, - pixel_aspect, - scale_factor_by_width, - scale_factor_by_height - ) - ) - # scaling none square pixels and 1920 width if ( input_height != output_height @@ -1319,6 +1346,16 @@ class ExtractReview(pyblish.api.InstancePlugin): "setsar=1" ]) + # letter_box + if letter_box_enabled: + filters.extend( + self.get_letterbox_filters( + letter_box_def, + output_width, + output_height + ) + ) + new_repre["resolutionWidth"] = output_width new_repre["resolutionHeight"] = output_height diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index ec836954e8..60245314f4 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -7,7 +7,7 @@ import shutil from pymongo import InsertOne, ReplaceOne import pyblish.api from avalon import api, io, schema -from avalon.vendor import filelink +from openpype.lib import create_hard_link class IntegrateHeroVersion(pyblish.api.InstancePlugin): @@ -518,7 +518,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): # First try hardlink and copy if paths are cross drive try: - filelink.create(src_path, dst_path, filelink.HARDLINK) + create_hard_link(src_path, dst_path) # Return when successful return diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6e0940d459..e8dab089af 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -13,12 +13,14 @@ from pymongo import DeleteOne, InsertOne import pyblish.api from avalon import io from avalon.api import format_template_with_optional_keys -from avalon.vendor import filelink import openpype.api from datetime import datetime # from pype.modules import ModulesManager from openpype.lib.profiles_filtering import filter_profiles -from openpype.lib import prepare_template_data +from openpype.lib import ( + prepare_template_data, + create_hard_link +) # this is needed until speedcopy for linux is fixed if sys.platform == "win32": @@ -192,11 +194,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "short": task_code } - else: + elif "task" in anatomy_data: # Just set 'task_name' variable to context task task_name = anatomy_data["task"]["name"] task_type = anatomy_data["task"]["type"] + else: + task_name = None + task_type = None + # Fill family in anatomy data anatomy_data["family"] = instance.data.get("family") @@ -730,7 +736,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): self.log.critical("An unexpected error occurred.") six.reraise(*sys.exc_info()) - filelink.create(src, dst, filelink.HARDLINK) + create_hard_link(src, dst) def get_subset(self, asset, instance): subset_name = instance.data["subset"] @@ -816,8 +822,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # - is there a chance that task name is not filled in anatomy # data? # - should we use context task in that case? - task_name = instance.data["anatomyData"]["task"]["name"] - task_type = instance.data["anatomyData"]["task"]["type"] + anatomy_data = instance.data["anatomyData"] + task_name = None + task_type = None + if "task" in anatomy_data: + task_name = anatomy_data["task"]["name"] + task_type = anatomy_data["task"]["type"] filtering_criteria = { "families": instance.data["family"], "hosts": instance.context.data["hostName"], diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index abf69645b7..874c08064a 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -6,6 +6,7 @@ import platform import json import opentimelineio_contrib.adapters.ffmpeg_burnins as ffmpeg_burnins import openpype.lib +from openpype.lib.vendor_bin_utils import get_fps ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") @@ -50,25 +51,6 @@ def _get_ffprobe_data(source): return json.loads(out) -def get_fps(str_value): - if str_value == "0/0": - print("WARNING: Source has \"r_frame_rate\" value set to \"0/0\".") - return "Unknown" - - items = str_value.split("/") - if len(items) == 1: - fps = float(items[0]) - - elif len(items) == 2: - fps = float(items[0]) / float(items[1]) - - # Check if fps is integer or float number - if int(fps) == fps: - fps = int(fps) - - return str(fps) - - def _prores_codec_args(stream_data, source_ffmpeg_cmd): output = [] diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index f08bee8b2d..9c44d9bc86 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -107,7 +107,6 @@ "letter_box": { "enabled": false, "ratio": 0.0, - "state": "letterbox", "fill_color": [ 0, 0, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index e608e9ff63..3eea7ccb30 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -366,19 +366,6 @@ "minimum": 0, "maximum": 10000 }, - { - "key": "state", - "label": "Type", - "type": "enum", - "enum_items": [ - { - "letterbox": "Letterbox" - }, - { - "pillar": "Pillar" - } - ] - }, { "type": "color", "label": "Fill Color", diff --git a/openpype/style/style.css b/openpype/style/style.css index ba40b780ab..5586cf766d 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1266,6 +1266,12 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { font-size: 15pt; font-weight: 750; } +#ChooseProjectFrame { + border-radius: 10px; +} +#ChooseProjectView { + background: transparent; +} /* Globally used names */ #Separator { diff --git a/openpype/tests/test_avalon_plugin_presets.py b/openpype/tests/test_avalon_plugin_presets.py index ec21385d23..f1b1a94713 100644 --- a/openpype/tests/test_avalon_plugin_presets.py +++ b/openpype/tests/test_avalon_plugin_presets.py @@ -1,8 +1,9 @@ import avalon.api as api import openpype +from openpype.pipeline import LegacyCreator -class MyTestCreator(api.Creator): +class MyTestCreator(LegacyCreator): my_test_property = "A" @@ -26,8 +27,8 @@ def test_avalon_plugin_presets(monkeypatch, printer): openpype.install() api.register_host(Test()) - api.register_plugin(api.Creator, MyTestCreator) - plugins = api.discover(api.Creator) + api.register_plugin(LegacyCreator, MyTestCreator) + plugins = api.discover(LegacyCreator) printer("Test if we got our test plugin") assert MyTestCreator in plugins for p in plugins: diff --git a/openpype/tools/creator/model.py b/openpype/tools/creator/model.py index 6907e8f0aa..ef61c6e0f0 100644 --- a/openpype/tools/creator/model.py +++ b/openpype/tools/creator/model.py @@ -2,6 +2,7 @@ import uuid from Qt import QtGui, QtCore from avalon import api +from openpype.pipeline import LegacyCreator from . constants import ( FAMILY_ROLE, @@ -21,7 +22,7 @@ class CreatorsModel(QtGui.QStandardItemModel): self._creators_by_id = {} items = [] - creators = api.discover(api.Creator) + creators = api.discover(LegacyCreator) for creator in creators: item_id = str(uuid.uuid4()) self._creators_by_id[item_id] = creator diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index 9dd435c1cc..43df08496b 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -3,9 +3,8 @@ import inspect from Qt import QtWidgets, QtCore, QtGui -from avalon.vendor import qtawesome +import qtawesome -from openpype import style from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from openpype.tools.utils import ErrorMessageBox diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index f1d0849dfe..51cc66e715 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -9,7 +9,12 @@ from avalon import api, io from openpype import style from openpype.api import get_current_project_settings from openpype.tools.utils.lib import qt_app_context -from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS +from openpype.pipeline.create import ( + SUBSET_NAME_ALLOWED_SYMBOLS, + legacy_create, + CreatorError, + LegacyCreator, +) from .model import CreatorsModel from .widgets import ( @@ -422,7 +427,7 @@ class CreatorWindow(QtWidgets.QDialog): error_info = None try: - api.create( + legacy_create( creator_plugin, subset_name, asset_name, @@ -430,7 +435,7 @@ class CreatorWindow(QtWidgets.QDialog): data={"variant": variant} ) - except api.CreatorError as exc: + except CreatorError as exc: self.echo("Creator error: {}".format(str(exc))) error_info = (str(exc), None) @@ -486,7 +491,7 @@ def show(debug=False, parent=None): if debug: from avalon import mock for creator in mock.creators: - api.register_plugin(api.Creator, creator) + api.register_plugin(LegacyCreator, creator) import traceback sys.excepthook = lambda typ, val, tb: traceback.print_last() diff --git a/openpype/tools/launcher/lib.py b/openpype/tools/launcher/lib.py index b4e6a0c3e9..68c759f295 100644 --- a/openpype/tools/launcher/lib.py +++ b/openpype/tools/launcher/lib.py @@ -16,7 +16,7 @@ provides a bridge between the file-based project inventory and configuration. import os from Qt import QtGui -from avalon.vendor import qtawesome +import qtawesome from openpype.api import resources ICON_CACHE = {} diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index effa283318..9036c9cbd5 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -7,7 +7,7 @@ import time import appdirs from Qt import QtCore, QtGui -from avalon.vendor import qtawesome +import qtawesome from avalon import api from openpype.lib import JSONSettingRegistry from openpype.lib.applications import ( diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 30e6531843..62599664fe 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -2,7 +2,7 @@ import copy import time import collections from Qt import QtWidgets, QtCore, QtGui -from avalon.vendor import qtawesome +import qtawesome from openpype.tools.flickcharm import FlickCharm from openpype.tools.utils.assets_widget import SingleSelectAssetsWidget diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index b5b6368865..d80b3eabf0 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -8,7 +8,7 @@ from avalon.api import AvalonMongoDB from openpype import style from openpype.api import resources -from avalon.vendor import qtawesome +import qtawesome from .models import ( LauncherModel, ProjectModel diff --git a/openpype/tools/loader/lib.py b/openpype/tools/loader/lib.py index 180dee3eb5..28e94237ec 100644 --- a/openpype/tools/loader/lib.py +++ b/openpype/tools/loader/lib.py @@ -1,7 +1,7 @@ import inspect from Qt import QtGui +import qtawesome -from avalon.vendor import qtawesome from openpype.tools.utils.widgets import ( OptionalAction, OptionDialog diff --git a/openpype/tools/loader/model.py b/openpype/tools/loader/model.py index 10b22d0e17..baee569239 100644 --- a/openpype/tools/loader/model.py +++ b/openpype/tools/loader/model.py @@ -8,8 +8,8 @@ from avalon import ( schema ) from Qt import QtCore, QtGui +import qtawesome -from avalon.vendor import qtawesome from avalon.lib import HeroVersionType from openpype.tools.utils.models import TreeModel, Item diff --git a/openpype/tools/mayalookassigner/models.py b/openpype/tools/mayalookassigner/models.py index 39cab83c61..386b7d7e1e 100644 --- a/openpype/tools/mayalookassigner/models.py +++ b/openpype/tools/mayalookassigner/models.py @@ -1,8 +1,8 @@ from collections import defaultdict from Qt import QtCore +import qtawesome -from avalon.vendor import qtawesome from avalon.style import colors from openpype.tools.utils import models diff --git a/openpype/tools/project_manager/project_manager/style.py b/openpype/tools/project_manager/project_manager/style.py index d24fc7102f..4405d05960 100644 --- a/openpype/tools/project_manager/project_manager/style.py +++ b/openpype/tools/project_manager/project_manager/style.py @@ -1,7 +1,7 @@ import os -from Qt import QtCore, QtGui +from Qt import QtGui -from avalon.vendor import qtawesome +import qtawesome from openpype.tools.utils import paint_image_with_color diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 5a84b1d8ca..6707feac9c 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -873,8 +873,6 @@ class PublisherController: """ for idx, plugin in enumerate(self.publish_plugins): self._publish_progress = idx - # Add plugin to publish report - self._publish_report.add_plugin_iter(plugin, self._publish_context) # Reset current plugin validations error self._publish_current_plugin_validation_errors = None @@ -902,6 +900,9 @@ class PublisherController: ): yield MainThreadItem(self.stop_publish) + # Add plugin to publish report + self._publish_report.add_plugin_iter(plugin, self._publish_context) + # Trigger callback that new plugin is going to be processed self._trigger_callbacks( self._publish_plugin_changed_callback_refs, plugin diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index c5b77eca8b..6396b77901 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -8,12 +8,14 @@ try: except Exception: commonmark = None from Qt import QtWidgets, QtCore, QtGui - +from openpype.lib import TaskNotSetError from openpype.pipeline.create import ( CreatorError, SUBSET_NAME_ALLOWED_SYMBOLS ) +from openpype.tools.utils import ErrorMessageBox + from .widgets import IconValuePixmapLabel from .assets_widget import CreateDialogAssetsWidget from .tasks_widget import CreateDialogTasksWidget @@ -27,7 +29,7 @@ from ..constants import ( SEPARATORS = ("---separator---", "---") -class CreateErrorMessageBox(QtWidgets.QDialog): +class CreateErrorMessageBox(ErrorMessageBox): def __init__( self, creator_label, @@ -35,24 +37,38 @@ class CreateErrorMessageBox(QtWidgets.QDialog): asset_name, exc_msg, formatted_traceback, - parent=None + parent ): - super(CreateErrorMessageBox, self).__init__(parent) - self.setWindowTitle("Creation failed") - self.setFocusPolicy(QtCore.Qt.StrongFocus) - if not parent: - self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint - ) + self._creator_label = creator_label + self._subset_name = subset_name + self._asset_name = asset_name + self._exc_msg = exc_msg + self._formatted_traceback = formatted_traceback + super(CreateErrorMessageBox, self).__init__("Creation failed", parent) - body_layout = QtWidgets.QVBoxLayout(self) - - main_label = ( + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( "Failed to create" ) - main_label_widget = QtWidgets.QLabel(main_label, self) - body_layout.addWidget(main_label_widget) + return label_widget + def _get_report_data(self): + report_message = ( + "{creator}: Failed to create Subset: \"{subset}\"" + " in Asset: \"{asset}\"" + "\n\nError: {message}" + ).format( + creator=self._creator_label, + subset=self._subset_name, + asset=self._asset_name, + message=self._exc_msg, + ) + if self._formatted_traceback: + report_message += "\n\n{}".format(self._formatted_traceback) + return [report_message] + + def _create_content(self, content_layout): item_name_template = ( "Creator: {}
" "Subset: {}
" @@ -61,48 +77,29 @@ class CreateErrorMessageBox(QtWidgets.QDialog): exc_msg_template = "{}" line = self._create_line() - body_layout.addWidget(line) + content_layout.addWidget(line) - item_name = item_name_template.format( - creator_label, subset_name, asset_name - ) - item_name_widget = QtWidgets.QLabel( - item_name.replace("\n", "
"), self - ) - body_layout.addWidget(item_name_widget) - - exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
")) - message_label_widget = QtWidgets.QLabel(exc_msg, self) - body_layout.addWidget(message_label_widget) - - if formatted_traceback: - tb_widget = QtWidgets.QLabel( - formatted_traceback.replace("\n", "
"), self + item_name_widget = QtWidgets.QLabel(self) + item_name_widget.setText( + item_name_template.format( + self._creator_label, self._subset_name, self._asset_name ) - tb_widget.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - body_layout.addWidget(tb_widget) - - footer_widget = QtWidgets.QWidget(self) - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - button_box = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical) - button_box.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Ok ) - button_box.accepted.connect(self._on_accept) - footer_layout.addWidget(button_box, alignment=QtCore.Qt.AlignRight) - body_layout.addWidget(footer_widget) + content_layout.addWidget(item_name_widget) - def _on_accept(self): - self.close() + message_label_widget = QtWidgets.QLabel(self) + message_label_widget.setText( + exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) + ) + content_layout.addWidget(message_label_widget) - def _create_line(self): - line = QtWidgets.QFrame(self) - line.setFixedHeight(2) - line.setFrameShape(QtWidgets.QFrame.HLine) - line.setFrameShadow(QtWidgets.QFrame.Sunken) - return line + if self._formatted_traceback: + line_widget = self._create_line() + tb_widget = self._create_traceback_widget( + self._formatted_traceback + ) + content_layout.addWidget(line_widget) + content_layout.addWidget(tb_widget) # TODO add creator identifier/label to details @@ -201,7 +198,7 @@ class CreateDialog(QtWidgets.QDialog): self._prereq_available = False - self.message_dialog = None + self._message_dialog = None name_pattern = "^[{}]*$".format(SUBSET_NAME_ALLOWED_SYMBOLS) self._name_pattern = name_pattern @@ -566,10 +563,9 @@ class CreateDialog(QtWidgets.QDialog): if variant_value is None: variant_value = self.variant_input.text() - match = self._compiled_name_pattern.match(variant_value) - valid = bool(match) - self.create_btn.setEnabled(valid) - if not valid: + self.create_btn.setEnabled(True) + if not self._compiled_name_pattern.match(variant_value): + self.create_btn.setEnabled(False) self._set_variant_state_property("invalid") self.subset_name_input.setText("< Invalid variant >") return @@ -579,9 +575,16 @@ class CreateDialog(QtWidgets.QDialog): asset_doc = copy.deepcopy(self._asset_doc) # Calculate subset name with Creator plugin - subset_name = self._selected_creator.get_subset_name( - variant_value, task_name, asset_doc, project_name - ) + try: + subset_name = self._selected_creator.get_subset_name( + variant_value, task_name, asset_doc, project_name + ) + except TaskNotSetError: + self.create_btn.setEnabled(False) + self._set_variant_state_property("invalid") + self.subset_name_input.setText("< Missing task >") + return + self.subset_name_input.setText(subset_name) self._validate_subset_name(subset_name, variant_value) @@ -694,14 +697,18 @@ class CreateDialog(QtWidgets.QDialog): "family": family } - error_info = None + error_msg = None + formatted_traceback = None try: self.controller.create( - creator_identifier, subset_name, instance_data, pre_create_data + creator_identifier, + subset_name, + instance_data, + pre_create_data ) except CreatorError as exc: - error_info = (str(exc), None) + error_msg = str(exc) # Use bare except because some hosts raise their exceptions that # do not inherit from python's `BaseException` @@ -710,12 +717,17 @@ class CreateDialog(QtWidgets.QDialog): formatted_traceback = "".join(traceback.format_exception( exc_type, exc_value, exc_traceback )) - error_info = (str(exc_value), formatted_traceback) + error_msg = str(exc_value) - if error_info: + if error_msg is not None: box = CreateErrorMessageBox( - creator_label, subset_name, asset_name, *error_info + creator_label, + subset_name, + asset_name, + error_msg, + formatted_traceback, + parent=self ) box.show() # Store dialog so is not garbage collected before is shown - self.message_dialog = box + self._message_dialog = box diff --git a/openpype/tools/publisher/widgets/list_view_widgets.py b/openpype/tools/publisher/widgets/list_view_widgets.py index 23a86cd070..6bddaf66c8 100644 --- a/openpype/tools/publisher/widgets/list_view_widgets.py +++ b/openpype/tools/publisher/widgets/list_view_widgets.py @@ -467,12 +467,22 @@ class InstanceListView(AbstractInstanceView): else: active = False + group_names = set() for instance_id in selected_instance_ids: widget = self._widgets_by_id.get(instance_id) - if widget is not None: - widget.set_active(active) + if widget is None: + continue + + widget.set_active(active) + group_name = self._group_by_instance_id.get(instance_id) + if group_name is not None: + group_names.add(group_name) + + for group_name in group_names: + self._update_group_checkstate(group_name) def _update_group_checkstate(self, group_name): + """Update checkstate of one group.""" widget = self._group_widgets.get(group_name) if widget is None: return diff --git a/openpype/tools/publisher/widgets/tasks_widget.py b/openpype/tools/publisher/widgets/tasks_widget.py index a0b3a340ae..2d1cc017af 100644 --- a/openpype/tools/publisher/widgets/tasks_widget.py +++ b/openpype/tools/publisher/widgets/tasks_widget.py @@ -17,9 +17,10 @@ class TasksModel(QtGui.QStandardItemModel): controller (PublisherController): Controller which handles creation and publishing. """ - def __init__(self, controller): + def __init__(self, controller, allow_empty_task=False): super(TasksModel, self).__init__() + self._allow_empty_task = allow_empty_task self._controller = controller self._items_by_name = {} self._asset_names = [] @@ -70,8 +71,14 @@ class TasksModel(QtGui.QStandardItemModel): task_name (str): Name of task which should be available in asset's tasks. """ - task_names = self._task_names_by_asset_name.get(asset_name) - if task_names and task_name in task_names: + if asset_name not in self._task_names_by_asset_name: + return False + + if self._allow_empty_task and not task_name: + return True + + task_names = self._task_names_by_asset_name[asset_name] + if task_name in task_names: return True return False @@ -92,6 +99,8 @@ class TasksModel(QtGui.QStandardItemModel): new_task_names = self.get_intersection_of_tasks( task_names_by_asset_name ) + if self._allow_empty_task: + new_task_names.add("") old_task_names = set(self._items_by_name.keys()) if new_task_names == old_task_names: return @@ -111,7 +120,9 @@ class TasksModel(QtGui.QStandardItemModel): item.setData(task_name, TASK_NAME_ROLE) self._items_by_name[task_name] = item new_items.append(item) - root_item.appendRows(new_items) + + if new_items: + root_item.appendRows(new_items) def headerData(self, section, orientation, role=None): if role is None: diff --git a/openpype/tools/publisher/widgets/widgets.py b/openpype/tools/publisher/widgets/widgets.py index fb1f0e54aa..4e55e86491 100644 --- a/openpype/tools/publisher/widgets/widgets.py +++ b/openpype/tools/publisher/widgets/widgets.py @@ -4,9 +4,9 @@ import re import copy import collections from Qt import QtWidgets, QtCore, QtGui +import qtawesome -from avalon.vendor import qtawesome - +from openpype.lib import TaskNotSetError from openpype.widgets.attribute_defs import create_widget_for_attr_def from openpype.tools import resources from openpype.tools.flickcharm import FlickCharm @@ -471,6 +471,28 @@ class AssetsField(BaseClickableFrame): self.set_selected_items(self._origin_value) +class TasksComboboxProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(TasksComboboxProxy, self).__init__(*args, **kwargs) + self._filter_empty = False + + def set_filter_empty(self, filter_empty): + if self._filter_empty is filter_empty: + return + self._filter_empty = filter_empty + self.invalidate() + + def filterAcceptsRow(self, source_row, parent_index): + if self._filter_empty: + model = self.sourceModel() + source_index = model.index( + source_row, self.filterKeyColumn(), parent_index + ) + if not source_index.data(QtCore.Qt.DisplayRole): + return False + return True + + class TasksCombobox(QtWidgets.QComboBox): """Combobox to show tasks for selected instances. @@ -490,13 +512,16 @@ class TasksCombobox(QtWidgets.QComboBox): delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(delegate) - model = TasksModel(controller) - self.setModel(model) + model = TasksModel(controller, True) + proxy_model = TasksComboboxProxy() + proxy_model.setSourceModel(model) + self.setModel(proxy_model) self.currentIndexChanged.connect(self._on_index_change) self._delegate = delegate self._model = model + self._proxy_model = proxy_model self._origin_value = [] self._origin_selection = [] self._selected_items = [] @@ -507,6 +532,14 @@ class TasksCombobox(QtWidgets.QComboBox): self._text = None + def set_invalid_empty_task(self, invalid=True): + self._proxy_model.set_filter_empty(invalid) + if invalid: + self._set_is_valid(False) + self.set_text("< One or more subsets require Task selected >") + else: + self.set_text(None) + def set_multiselection_text(self, text): """Change text shown when multiple different tasks are in context.""" self._multiselection_text = text @@ -596,6 +629,8 @@ class TasksCombobox(QtWidgets.QComboBox): self._ignore_index_change = True self._model.set_asset_names(asset_names) + self._proxy_model.set_filter_empty(False) + self._proxy_model.sort(0) self._ignore_index_change = False @@ -641,6 +676,9 @@ class TasksCombobox(QtWidgets.QComboBox): asset_task_combinations (list): List of tuples. Each item in the list contain asset name and task name. """ + self._proxy_model.set_filter_empty(False) + self._proxy_model.sort(0) + if asset_task_combinations is None: asset_task_combinations = [] @@ -932,7 +970,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): family_value_widget.set_value() subset_value_widget.set_value() - submit_btn = QtWidgets.QPushButton("Submit", self) + submit_btn = QtWidgets.QPushButton("Confirm", self) cancel_btn = QtWidgets.QPushButton("Cancel", self) submit_btn.setEnabled(False) cancel_btn.setEnabled(False) @@ -998,7 +1036,33 @@ class GlobalAttrsWidget(QtWidgets.QWidget): project_name = self.controller.project_name subset_names = set() + invalid_tasks = False for instance in self._current_instances: + new_variant_value = instance.get("variant") + new_asset_name = instance.get("asset") + new_task_name = instance.get("task") + if variant_value is not None: + new_variant_value = variant_value + + if asset_name is not None: + new_asset_name = asset_name + + if task_name is not None: + new_task_name = task_name + + asset_doc = asset_docs_by_name[new_asset_name] + + try: + new_subset_name = instance.creator.get_subset_name( + new_variant_value, new_task_name, asset_doc, project_name + ) + except TaskNotSetError: + invalid_tasks = True + instance.set_task_invalid(True) + subset_names.add(instance["subset"]) + continue + + subset_names.add(new_subset_name) if variant_value is not None: instance["variant"] = variant_value @@ -1007,25 +1071,18 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance.set_asset_invalid(False) if task_name is not None: - instance["task"] = task_name + instance["task"] = task_name or None instance.set_task_invalid(False) - new_variant_value = instance.get("variant") - new_asset_name = instance.get("asset") - new_task_name = instance.get("task") - - asset_doc = asset_docs_by_name[new_asset_name] - - new_subset_name = instance.creator.get_subset_name( - new_variant_value, new_task_name, asset_doc, project_name - ) - subset_names.add(new_subset_name) instance["subset"] = new_subset_name + if invalid_tasks: + self.task_value_widget.set_invalid_empty_task() + self.subset_value_widget.set_value(subset_names) self._set_btns_enabled(False) - self._set_btns_visible(False) + self._set_btns_visible(invalid_tasks) self.instance_context_changed.emit() @@ -1098,7 +1155,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): variants.add(instance.get("variant") or self.unknown_value) families.add(instance.get("family") or self.unknown_value) asset_name = instance.get("asset") or self.unknown_value - task_name = instance.get("task") or self.unknown_value + task_name = instance.get("task") or "" asset_names.add(asset_name) asset_task_combinations.append((asset_name, task_name)) subset_names.add(instance.get("subset") or self.unknown_value) diff --git a/openpype/tools/pyblish_pype/model.py b/openpype/tools/pyblish_pype/model.py index 0faadb5940..2931a379b3 100644 --- a/openpype/tools/pyblish_pype/model.py +++ b/openpype/tools/pyblish_pype/model.py @@ -29,10 +29,9 @@ import pyblish from . import settings, util from .awesome import tags as awesome -import Qt from Qt import QtCore, QtGui +import qtawesome from six import text_type -from .vendor import qtawesome from .constants import PluginStates, InstanceStates, GroupStates, Roles from openpype.api import get_system_settings diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 6435e5c488..cba60be355 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -4,9 +4,9 @@ import logging from collections import defaultdict from Qt import QtCore, QtGui -from avalon import api, io, style, schema -from avalon.vendor import qtawesome +import qtawesome +from avalon import api, io, style, schema from avalon.lib import HeroVersionType from openpype.tools.utils.models import TreeModel, Item diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index 4946c073d4..93ea68beb4 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -1,9 +1,9 @@ import collections import logging from Qt import QtWidgets, QtCore +import qtawesome from avalon import io, api, pipeline -from avalon.vendor import qtawesome from .widgets import ( ButtonWithMenu, diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index ec48b10e47..32c1883de6 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -3,9 +3,9 @@ import logging from functools import partial from Qt import QtWidgets, QtCore +import qtawesome from avalon import io, api, style -from avalon.vendor import qtawesome from avalon.lib import HeroVersionType from openpype.modules import ModulesManager diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 095d30cac0..83e4435015 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -2,7 +2,7 @@ import os import sys from Qt import QtWidgets, QtCore -from avalon.vendor import qtawesome +import qtawesome from avalon import io, api from openpype import style diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 663d497c36..a5b5cd40f0 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -1,9 +1,9 @@ -import os import sys import traceback import contextlib from enum import Enum from Qt import QtWidgets, QtCore +import qtawesome from openpype.lib import get_openpype_version from openpype.tools.utils import set_style_property @@ -63,7 +63,6 @@ from .item_widgets import ( PathInputWidget ) from .color_widget import ColorWidget -from avalon.vendor import qtawesome class CategoryState(Enum): diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index f793aab057..577c2630ab 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -2,7 +2,7 @@ import os import copy import uuid from Qt import QtWidgets, QtCore, QtGui -from avalon.vendor import qtawesome +import qtawesome from avalon.mongodb import ( AvalonMongoConnection, AvalonMongoDB diff --git a/openpype/tools/standalonepublish/widgets/model_asset.py b/openpype/tools/standalonepublish/widgets/model_asset.py index 60afe8f96c..6d764eff9f 100644 --- a/openpype/tools/standalonepublish/widgets/model_asset.py +++ b/openpype/tools/standalonepublish/widgets/model_asset.py @@ -1,8 +1,8 @@ import logging import collections from Qt import QtCore, QtGui +import qtawesome from . import TreeModel, Node -from avalon.vendor import qtawesome from avalon import style diff --git a/openpype/tools/standalonepublish/widgets/model_tasks_template.py b/openpype/tools/standalonepublish/widgets/model_tasks_template.py index 476f45391d..1f36eaa39d 100644 --- a/openpype/tools/standalonepublish/widgets/model_tasks_template.py +++ b/openpype/tools/standalonepublish/widgets/model_tasks_template.py @@ -1,6 +1,6 @@ from Qt import QtCore +import qtawesome from . import Node, TreeModel -from avalon.vendor import qtawesome from avalon import style diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 2886d600bf..d929f227f9 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -1,9 +1,9 @@ import contextlib from Qt import QtWidgets, QtCore +import qtawesome from openpype.tools.utils import PlaceholderLineEdit -from avalon.vendor import qtawesome from avalon import style from . import RecursiveSortFilterProxyModel, AssetModel diff --git a/openpype/tools/standalonepublish/widgets/widget_family.py b/openpype/tools/standalonepublish/widgets/widget_family.py index ae44899a89..08cd45bbf2 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family.py +++ b/openpype/tools/standalonepublish/widgets/widget_family.py @@ -1,14 +1,11 @@ -import os import re from Qt import QtWidgets, QtCore from . import HelpRole, FamilyRole, ExistsRole, PluginRole, PluginKeyRole from . import FamilyDescriptionWidget -from openpype.api import ( - get_project_settings, - Creator -) +from openpype.api import get_project_settings +from openpype.pipeline import LegacyCreator from openpype.lib import TaskNotSetError from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS @@ -390,7 +387,7 @@ class FamilyWidget(QtWidgets.QWidget): sp_settings = settings.get('standalonepublisher', {}) for key, creator_data in sp_settings.get("create", {}).items(): - creator = type(key, (Creator, ), creator_data) + creator = type(key, (LegacyCreator, ), creator_data) label = creator.label or creator.family item = QtWidgets.QListWidgetItem(label) diff --git a/openpype/tools/standalonepublish/widgets/widget_family_desc.py b/openpype/tools/standalonepublish/widgets/widget_family_desc.py index 8c95ddf2e4..79681615b9 100644 --- a/openpype/tools/standalonepublish/widgets/widget_family_desc.py +++ b/openpype/tools/standalonepublish/widgets/widget_family_desc.py @@ -1,7 +1,7 @@ -from Qt import QtWidgets, QtCore, QtGui -from . import FamilyRole, PluginRole -from avalon.vendor import qtawesome import six +from Qt import QtWidgets, QtCore, QtGui +import qtawesome +from . import FamilyRole, PluginRole class FamilyDescriptionWidget(QtWidgets.QWidget): diff --git a/openpype/tools/subsetmanager/window.py b/openpype/tools/subsetmanager/window.py index b7430d0626..a53af52174 100644 --- a/openpype/tools/subsetmanager/window.py +++ b/openpype/tools/subsetmanager/window.py @@ -2,9 +2,9 @@ import os import sys from Qt import QtWidgets, QtCore +import qtawesome from avalon import api -from avalon.vendor import qtawesome from openpype import style from openpype.tools.utils import PlaceholderLineEdit diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index 53f8ca450a..d0453c4f23 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -28,38 +28,50 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): super(StandaloneOverlayWidget, self).__init__(publisher_window) self.setObjectName("OverlayFrame") + middle_frame = QtWidgets.QFrame(self) + middle_frame.setObjectName("ChooseProjectFrame") + + content_widget = QtWidgets.QWidget(middle_frame) + # Create db connection for projects model dbcon = AvalonMongoDB() dbcon.install() - header_label = QtWidgets.QLabel("Choose project", self) + header_label = QtWidgets.QLabel("Choose project", content_widget) header_label.setObjectName("ChooseProjectLabel") # Create project models and view projects_model = ProjectModel(dbcon) projects_proxy = ProjectSortFilterProxy() projects_proxy.setSourceModel(projects_model) - projects_view = QtWidgets.QListView(self) + projects_view = QtWidgets.QListView(content_widget) + projects_view.setObjectName("ChooseProjectView") projects_view.setModel(projects_proxy) projects_view.setEditTriggers( QtWidgets.QAbstractItemView.NoEditTriggers ) - confirm_btn = QtWidgets.QPushButton("Choose", self) + confirm_btn = QtWidgets.QPushButton("Confirm", content_widget) btns_layout = QtWidgets.QHBoxLayout() btns_layout.addStretch(1) btns_layout.addWidget(confirm_btn, 0) - layout = QtWidgets.QGridLayout(self) - layout.addWidget(header_label, 0, 1, alignment=QtCore.Qt.AlignCenter) - layout.addWidget(projects_view, 1, 1) - layout.addLayout(btns_layout, 2, 1) - layout.setColumnStretch(0, 1) - layout.setColumnStretch(1, 0) - layout.setColumnStretch(2, 1) - layout.setRowStretch(0, 0) - layout.setRowStretch(1, 1) - layout.setRowStretch(2, 0) + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(20) + content_layout.addWidget(header_label, 0) + content_layout.addWidget(projects_view, 1) + content_layout.addLayout(btns_layout, 0) + + middle_layout = QtWidgets.QHBoxLayout(middle_frame) + middle_layout.setContentsMargins(30, 30, 10, 10) + middle_layout.addWidget(content_widget) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.addStretch(1) + main_layout.addWidget(middle_frame, 2) + main_layout.addStretch(1) projects_view.doubleClicked.connect(self._on_double_click) confirm_btn.clicked.connect(self._on_confirm_click) diff --git a/openpype/tools/utils/assets_widget.py b/openpype/tools/utils/assets_widget.py index 17164d9e0f..d410b0f1c3 100644 --- a/openpype/tools/utils/assets_widget.py +++ b/openpype/tools/utils/assets_widget.py @@ -3,9 +3,9 @@ import collections import Qt from Qt import QtWidgets, QtCore, QtGui +import qtawesome from avalon import style -from avalon.vendor import qtawesome from openpype.style import get_objected_colors from openpype.tools.flickcharm import FlickCharm diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 01b9e25ef3..1cbc632804 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -4,10 +4,10 @@ import contextlib import collections from Qt import QtWidgets, QtCore, QtGui +import qtawesome import avalon.api from avalon import style -from avalon.vendor import qtawesome from openpype.api import ( get_project_settings, diff --git a/openpype/tools/utils/tasks_widget.py b/openpype/tools/utils/tasks_widget.py index 2a8a45626c..7619f59974 100644 --- a/openpype/tools/utils/tasks_widget.py +++ b/openpype/tools/utils/tasks_widget.py @@ -1,7 +1,7 @@ from Qt import QtWidgets, QtCore, QtGui +import qtawesome from avalon import style -from avalon.vendor import qtawesome from .views import DeselectableTreeView @@ -14,6 +14,7 @@ TASK_ASSIGNEE_ROLE = QtCore.Qt.UserRole + 4 class TasksModel(QtGui.QStandardItemModel): """A model listing the tasks combined for a list of assets""" + def __init__(self, dbcon, parent=None): super(TasksModel, self).__init__(parent=parent) self.dbcon = dbcon diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index a4e172ea5c..d5ae909be8 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -1,8 +1,8 @@ import logging from Qt import QtWidgets, QtCore, QtGui - -from avalon.vendor import qtawesome, qargparse +import qargparse +import qtawesome from openpype.style import ( get_objected_colors, get_style_image_path diff --git a/openpype/tools/workfiles/model.py b/openpype/tools/workfiles/model.py index 3425cc3df0..b3cf5063e7 100644 --- a/openpype/tools/workfiles/model.py +++ b/openpype/tools/workfiles/model.py @@ -2,9 +2,9 @@ import os import logging from Qt import QtCore +import qtawesome from avalon import style -from avalon.vendor import qtawesome from openpype.tools.utils.models import TreeModel, Item log = logging.getLogger(__name__) diff --git a/openpype/vendor/python/common/qargparse.py b/openpype/vendor/python/common/qargparse.py new file mode 100644 index 0000000000..ebde9ae76d --- /dev/null +++ b/openpype/vendor/python/common/qargparse.py @@ -0,0 +1,817 @@ +""" +NOTE: The required `Qt` module has changed to use the one that vendorized. + Remember to change to relative import when updating this. +""" + +import re +import logging + +from collections import OrderedDict as odict +from Qt import QtCore, QtWidgets, QtGui +import qtawesome + +__version__ = "0.5.2" +_log = logging.getLogger(__name__) +_type = type # used as argument + +try: + # Python 2 + _basestring = basestring +except NameError: + _basestring = str + + +class QArgumentParser(QtWidgets.QWidget): + """User interface arguments + + Arguments: + arguments (list, optional): Instances of QArgument + description (str, optional): Long-form text of what this parser is for + storage (QSettings, optional): Persistence to disk, providing + value() and setValue() methods + + """ + + changed = QtCore.Signal(QtCore.QObject) # A QArgument + + def __init__(self, + arguments=None, + description=None, + storage=None, + parent=None): + super(QArgumentParser, self).__init__(parent) + self.setAttribute(QtCore.Qt.WA_StyledBackground) + + # Create internal settings + if storage is True: + storage = QtCore.QSettings( + QtCore.QSettings.IniFormat, + QtCore.QSettings.UserScope, + __name__, "QArgparse", + ) + + if storage is not None: + _log.info("Storing settings @ %s" % storage.fileName()) + + arguments = arguments or [] + + assert hasattr(arguments, "__iter__"), "arguments must be iterable" + assert isinstance(storage, (type(None), QtCore.QSettings)), ( + "storage must be of type QSettings" + ) + + layout = QtWidgets.QGridLayout(self) + layout.setRowStretch(999, 1) + + if description: + layout.addWidget(QtWidgets.QLabel(description), 0, 0, 1, 2) + + self._row = 1 + self._storage = storage + self._arguments = odict() + self._desciption = description + + for arg in arguments or []: + self._addArgument(arg) + + self.setStyleSheet(style) + + def setDescription(self, text): + self._desciption.setText(text) + + def addArgument(self, name, type=None, default=None, **kwargs): + # Infer type from default + if type is None and default is not None: + type = _type(default) + + # Default to string + type = type or str + + Argument = { + None: String, + int: Integer, + float: Float, + bool: Boolean, + str: String, + list: Enum, + tuple: Enum, + }.get(type, type) + + arg = Argument(name, default=default, **kwargs) + self._addArgument(arg) + return arg + + def _addArgument(self, arg): + if arg["name"] in self._arguments: + raise ValueError("Duplicate argument '%s'" % arg["name"]) + + if self._storage is not None: + default = self._storage.value(arg["name"]) + + if default: + if isinstance(arg, Boolean): + default = bool({ + None: QtCore.Qt.Unchecked, + + 0: QtCore.Qt.Unchecked, + 1: QtCore.Qt.Checked, + 2: QtCore.Qt.Checked, + + "0": QtCore.Qt.Unchecked, + "1": QtCore.Qt.Checked, + "2": QtCore.Qt.Checked, + + # May be stored as string, if used with IniFormat + "false": QtCore.Qt.Unchecked, + "true": QtCore.Qt.Checked, + }.get(default)) + + arg["default"] = default + + arg.changed.connect(lambda: self.on_changed(arg)) + + label = ( + QtWidgets.QLabel(arg["label"]) + if arg.label + else QtWidgets.QLabel() + ) + widget = arg.create() + icon = qtawesome.icon("fa.refresh", color="white") + reset = QtWidgets.QPushButton(icon, "") # default + reset.setToolTip("Reset") + reset.setProperty("type", "reset") + reset.clicked.connect(lambda: self.on_reset(arg)) + + # Shown on edit + reset.hide() + + for widget in (label, widget): + widget.setToolTip(arg["help"]) + widget.setObjectName(arg["name"]) # useful in CSS + widget.setProperty("type", type(arg).__name__) + widget.setAttribute(QtCore.Qt.WA_StyledBackground) + widget.setEnabled(arg["enabled"]) + + # Align label on top of row if widget is over two times heiger + height = (lambda w: w.sizeHint().height()) + label_on_top = height(label) * 2 < height(widget) + alignment = (QtCore.Qt.AlignTop,) if label_on_top else () + + layout = self.layout() + layout.addWidget(label, self._row, 0, *alignment) + layout.addWidget(widget, self._row, 1) + layout.addWidget(reset, self._row, 2, *alignment) + layout.setColumnStretch(1, 1) + + def on_changed(*_): + reset.setVisible(arg["edited"]) + + arg.changed.connect(on_changed) + + self._row += 1 + self._arguments[arg["name"]] = arg + + def clear(self): + assert self._storage, "Cannot clear without persistent storage" + self._storage.clear() + _log.info("Clearing settings @ %s" % self._storage.fileName()) + + def find(self, name): + return self._arguments[name] + + def on_reset(self, arg): + arg.write(arg["default"]) + + def on_changed(self, arg): + arg["edited"] = arg.read() != arg["default"] + self.changed.emit(arg) + + # Optional PEP08 syntax + add_argument = addArgument + + +class QArgument(QtCore.QObject): + """Base class of argument user interface + """ + changed = QtCore.Signal() + + # Provide a left-hand side label for this argument + label = True + # For defining default value for each argument type + default = None + + def __init__(self, name, default=None, **kwargs): + super(QArgument, self).__init__(kwargs.pop("parent", None)) + + kwargs["name"] = name + kwargs["label"] = kwargs.get("label", camel_to_title(name)) + kwargs["default"] = self.default if default is None else default + kwargs["help"] = kwargs.get("help", "") + kwargs["read"] = kwargs.get("read") + kwargs["write"] = kwargs.get("write") + kwargs["enabled"] = bool(kwargs.get("enabled", True)) + kwargs["edited"] = False + + self._data = kwargs + + def __str__(self): + return self["name"] + + def __repr__(self): + return "%s(\"%s\")" % (type(self).__name__, self["name"]) + + def __getitem__(self, key): + return self._data[key] + + def __setitem__(self, key, value): + self._data[key] = value + + def __eq__(self, other): + if isinstance(other, _basestring): + return self["name"] == other + return super(QArgument, self).__eq__(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def create(self): + return QtWidgets.QWidget() + + def read(self): + return self._read() + + def write(self, value): + self._write(value) + self.changed.emit() + + +class Boolean(QArgument): + """Boolean type user interface + + Presented by `QtWidgets.QCheckBox`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (bool, optional): Argument's default value, default None + enabled (bool, optional): Whether to enable this widget, default True + + """ + def create(self): + widget = QtWidgets.QCheckBox() + widget.clicked.connect(self.changed.emit) + + if isinstance(self, Tristate): + self._read = lambda: widget.checkState() + state = { + 0: QtCore.Qt.Unchecked, + 1: QtCore.Qt.PartiallyChecked, + 2: QtCore.Qt.Checked, + "1": QtCore.Qt.PartiallyChecked, + "0": QtCore.Qt.Unchecked, + "2": QtCore.Qt.Checked, + } + else: + self._read = lambda: bool(widget.checkState()) + state = { + None: QtCore.Qt.Unchecked, + + 0: QtCore.Qt.Unchecked, + 1: QtCore.Qt.Checked, + 2: QtCore.Qt.Checked, + + "0": QtCore.Qt.Unchecked, + "1": QtCore.Qt.Checked, + "2": QtCore.Qt.Checked, + + # May be stored as string, if used with QSettings(..IniFormat) + "false": QtCore.Qt.Unchecked, + "true": QtCore.Qt.Checked, + } + + self._write = lambda value: widget.setCheckState(state[value]) + widget.clicked.connect(self.changed.emit) + + if self["default"] is not None: + self._write(self["default"]) + + return widget + + def read(self): + return self._read() + + +class Tristate(QArgument): + """Not implemented""" + + +class Number(QArgument): + """Base class of numeric type user interface""" + default = 0 + + def create(self): + if isinstance(self, Float): + widget = QtWidgets.QDoubleSpinBox() + widget.setMinimum(self._data.get("min", 0.0)) + widget.setMaximum(self._data.get("max", 99.99)) + else: + widget = QtWidgets.QSpinBox() + widget.setMinimum(self._data.get("min", 0)) + widget.setMaximum(self._data.get("max", 99)) + + widget.editingFinished.connect(self.changed.emit) + self._read = lambda: widget.value() + self._write = lambda value: widget.setValue(value) + + if self["default"] != self.default: + self._write(self["default"]) + + return widget + + +class Integer(Number): + """Integer type user interface + + A subclass of `qargparse.Number`, presented by `QtWidgets.QSpinBox`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (int, optional): Argument's default value, default 0 + min (int, optional): Argument's minimum value, default 0 + max (int, optional): Argument's maximum value, default 99 + enabled (bool, optional): Whether to enable this widget, default True + + """ + + +class Float(Number): + """Float type user interface + + A subclass of `qargparse.Number`, presented by `QtWidgets.QDoubleSpinBox`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (float, optional): Argument's default value, default 0.0 + min (float, optional): Argument's minimum value, default 0.0 + max (float, optional): Argument's maximum value, default 99.99 + enabled (bool, optional): Whether to enable this widget, default True + + """ + + +class Range(Number): + """Range type user interface + + A subclass of `qargparse.Number`, not production ready. + + """ + + +class Double3(QArgument): + """Double3 type user interface + + Presented by three `QtWidgets.QLineEdit` widget with `QDoubleValidator` + installed. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (tuple or list, optional): Default (0, 0, 0). + enabled (bool, optional): Whether to enable this widget, default True + + """ + default = (0, 0, 0) + + def create(self): + widget = QtWidgets.QWidget() + layout = QtWidgets.QHBoxLayout(widget) + layout.setContentsMargins(0, 0, 0, 0) + x, y, z = (self.child_arg(layout, i) for i in range(3)) + + self._read = lambda: ( + float(x.text()), float(y.text()), float(z.text())) + self._write = lambda value: [ + w.setText(str(float(v))) for w, v in zip([x, y, z], value)] + + if self["default"] != self.default: + self._write(self["default"]) + + return widget + + def child_arg(self, layout, index): + widget = QtWidgets.QLineEdit() + widget.setValidator(QtGui.QDoubleValidator()) + + default = str(float(self["default"][index])) + widget.setText(default) + + def focusOutEvent(event): + if not widget.text(): + widget.setText(default) # Ensure value exists for `_read` + QtWidgets.QLineEdit.focusOutEvent(widget, event) + widget.focusOutEvent = focusOutEvent + + widget.editingFinished.connect(self.changed.emit) + widget.returnPressed.connect(widget.editingFinished.emit) + + layout.addWidget(widget) + + return widget + + +class String(QArgument): + """String type user interface + + Presented by `QtWidgets.QLineEdit`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (str, optional): Argument's default value, default None + placeholder (str, optional): Placeholder message for the widget + enabled (bool, optional): Whether to enable this widget, default True + + """ + def __init__(self, *args, **kwargs): + super(String, self).__init__(*args, **kwargs) + self._previous = None + + def create(self): + widget = QtWidgets.QLineEdit() + widget.editingFinished.connect(self.onEditingFinished) + widget.returnPressed.connect(widget.editingFinished.emit) + self._read = lambda: widget.text() + self._write = lambda value: widget.setText(value) + + if isinstance(self, Info): + widget.setReadOnly(True) + widget.setPlaceholderText(self._data.get("placeholder", "")) + + if self["default"] is not None: + self._write(self["default"]) + self._previous = self["default"] + + return widget + + def onEditingFinished(self): + current = self._read() + + if current != self._previous: + self.changed.emit() + self._previous = current + + +class Info(String): + """String type user interface but read-only + + A subclass of `qargparse.String`, presented by `QtWidgets.QLineEdit`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (str, optional): Argument's default value, default None + enabled (bool, optional): Whether to enable this widget, default True + + """ + + +class Color(String): + """Color type user interface + + A subclass of `qargparse.String`, not production ready. + + """ + + +class Button(QArgument): + """Button type user interface + + Presented by `QtWidgets.QPushButton`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (bool, optional): Argument's default value, default None + enabled (bool, optional): Whether to enable this widget, default True + + """ + label = False + + def create(self): + widget = QtWidgets.QPushButton(self["label"]) + widget.clicked.connect(self.changed.emit) + + state = [ + QtCore.Qt.Unchecked, + QtCore.Qt.Checked, + ] + + if isinstance(self, Toggle): + widget.setCheckable(True) + if hasattr(widget, "isChecked"): + self._read = lambda: state[int(widget.isChecked())] + self._write = ( + lambda value: widget.setChecked(value) + ) + else: + self._read = lambda: widget.checkState() + self._write = ( + lambda value: widget.setCheckState(state[int(value)]) + ) + else: + self._read = lambda: "clicked" + self._write = lambda value: None + + if self["default"] is not None: + self._write(self["default"]) + + return widget + + +class Toggle(Button): + """Checkable `Button` type user interface + + Presented by `QtWidgets.QPushButton`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + default (bool, optional): Argument's default value, default None + enabled (bool, optional): Whether to enable this widget, default True + + """ + + +class InfoList(QArgument): + """String list type user interface + + Presented by `QtWidgets.QListView`, not production ready. + + """ + def __init__(self, name, **kwargs): + kwargs["default"] = kwargs.pop("default", ["Empty"]) + super(InfoList, self).__init__(name, **kwargs) + + def create(self): + class Model(QtCore.QStringListModel): + def data(self, index, role): + return super(Model, self).data(index, role) + + model = QtCore.QStringListModel(self["default"]) + widget = QtWidgets.QListView() + widget.setModel(model) + widget.setEditTriggers(widget.NoEditTriggers) + + self._read = lambda: model.stringList() + self._write = lambda value: model.setStringList(value) + + return widget + + +class Choice(QArgument): + """Argument user interface for selecting one from list + + Presented by `QtWidgets.QListView`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + items (list, optional): List of strings for select, default `["Empty"]` + default (str, optional): Default item in `items`, use first of `items` + if not given. + enabled (bool, optional): Whether to enable this widget, default True + + """ + def __init__(self, name, **kwargs): + kwargs["items"] = kwargs.get("items", ["Empty"]) + kwargs["default"] = kwargs.pop("default", kwargs["items"][0]) + super(Choice, self).__init__(name, **kwargs) + + def index(self, value): + """Return numerical equivalent to self.read()""" + return self["items"].index(value) + + def create(self): + def on_changed(selected, deselected): + try: + selected = selected.indexes()[0] + except IndexError: + # At least one item must be selected at all times + selected = deselected.indexes()[0] + + value = selected.data(QtCore.Qt.DisplayRole) + set_current(value) + self.changed.emit() + + def set_current(current): + options = model.stringList() + + if current == "Empty": + index = 0 + else: + for index, member in enumerate(options): + if member == current: + break + else: + raise ValueError( + "%s not a member of %s" % (current, options) + ) + + qindex = model.index(index, 0, QtCore.QModelIndex()) + smodel.setCurrentIndex(qindex, smodel.ClearAndSelect) + self["current"] = options[index] + + def reset(items, default=None): + items = items or ["Empty"] + model.setStringList(items) + set_current(default or items[0]) + + model = QtCore.QStringListModel() + widget = QtWidgets.QListView() + widget.setModel(model) + widget.setEditTriggers(widget.NoEditTriggers) + widget.setSelectionMode(widget.SingleSelection) + smodel = widget.selectionModel() + smodel.selectionChanged.connect(on_changed) + + self._read = lambda: self["current"] + self._write = lambda value: set_current(value) + self.reset = reset + + reset(self["items"], self["default"]) + + return widget + + +class Separator(QArgument): + """Visual separator + + Example: + + item1 + item2 + ------------ + item3 + item4 + + """ + + def create(self): + widget = QtWidgets.QWidget() + + self._read = lambda: None + self._write = lambda value: None + + return widget + + +class Enum(QArgument): + """Argument user interface for selecting one from dropdown list + + Presented by `QtWidgets.QComboBox`. + + Arguments: + name (str): The name of argument + label (str, optional): Display name, convert from `name` if not given + help (str, optional): Tool tip message of this argument + items (list, optional): List of strings for select, default `[]` + default (int, optional): Index of default item, use first of `items` + if not given. + enabled (bool, optional): Whether to enable this widget, default True + + """ + def __init__(self, name, **kwargs): + kwargs["default"] = kwargs.pop("default", 0) + kwargs["items"] = kwargs.get("items", []) + + assert isinstance(kwargs["items"], (tuple, list)), ( + "items must be list" + ) + + super(Enum, self).__init__(name, **kwargs) + + def create(self): + widget = QtWidgets.QComboBox() + widget.addItems(self["items"]) + widget.currentIndexChanged.connect( + lambda index: self.changed.emit()) + + self._read = lambda: widget.currentText() + self._write = lambda value: widget.setCurrentIndex(value) + + if self["default"] is not None: + self._write(self["default"]) + + return widget + + +style = """\ +QWidget { + /* Explicitly specify a size, to account for automatic HDPi */ + font-size: 11px; +} + +*[type="Button"] { + text-align:left; +} + +*[type="Info"] { + background: transparent; + border: none; +} + +QLabel[type="Separator"] { + min-height: 20px; + text-decoration: underline; +} + +QPushButton[type="reset"] { + max-width: 11px; + max-height: 11px; +} + +""" + + +def camelToTitle(text): + """Convert camelCase `text` to Title Case + + Example: + >>> camelToTitle("mixedCase") + "Mixed Case" + >>> camelToTitle("myName") + "My Name" + >>> camelToTitle("you") + "You" + >>> camelToTitle("You") + "You" + >>> camelToTitle("This is That") + "This Is That" + + """ + + return re.sub( + r"((?<=[a-z])[A-Z]|(?= 3 + except: + import thread as _thread # Python < 3 + except ImportError: + import _dummy_thread as _thread + +""" +# Exports only things specified by thread documentation; +# skipping obsolete synonyms allocate(), start_new(), exit_thread(). +__all__ = ['error', 'start_new_thread', 'exit', 'get_ident', 'allocate_lock', + 'interrupt_main', 'LockType'] + +# A dummy value +TIMEOUT_MAX = 2**31 + +# NOTE: this module can be imported early in the extension building process, +# and so top level imports of other modules should be avoided. Instead, all +# imports are done when needed on a function-by-function basis. Since threads +# are disabled, the import lock should not be an issue anyway (??). + +class error(Exception): + """Dummy implementation of _thread.error.""" + + def __init__(self, *args): + self.args = args + +def start_new_thread(function, args, kwargs={}): + """Dummy implementation of _thread.start_new_thread(). + + Compatibility is maintained by making sure that ``args`` is a + tuple and ``kwargs`` is a dictionary. If an exception is raised + and it is SystemExit (which can be done by _thread.exit()) it is + caught and nothing is done; all other exceptions are printed out + by using traceback.print_exc(). + + If the executed function calls interrupt_main the KeyboardInterrupt will be + raised when the function returns. + + """ + if type(args) != type(tuple()): + raise TypeError("2nd arg must be a tuple") + if type(kwargs) != type(dict()): + raise TypeError("3rd arg must be a dict") + global _main + _main = False + try: + function(*args, **kwargs) + except SystemExit: + pass + except: + import traceback + traceback.print_exc() + _main = True + global _interrupt + if _interrupt: + _interrupt = False + raise KeyboardInterrupt + +def exit(): + """Dummy implementation of _thread.exit().""" + raise SystemExit + +def get_ident(): + """Dummy implementation of _thread.get_ident(). + + Since this module should only be used when _threadmodule is not + available, it is safe to assume that the current process is the + only thread. Thus a constant can be safely returned. + """ + return -1 + +def allocate_lock(): + """Dummy implementation of _thread.allocate_lock().""" + return LockType() + +def stack_size(size=None): + """Dummy implementation of _thread.stack_size().""" + if size is not None: + raise error("setting thread stack size not supported") + return 0 + +class LockType(object): + """Class implementing dummy implementation of _thread.LockType. + + Compatibility is maintained by maintaining self.locked_status + which is a boolean that stores the state of the lock. Pickling of + the lock, though, should not be done since if the _thread module is + then used with an unpickled ``lock()`` from here problems could + occur from this class not having atomic methods. + + """ + + def __init__(self): + self.locked_status = False + + def acquire(self, waitflag=None, timeout=-1): + """Dummy implementation of acquire(). + + For blocking calls, self.locked_status is automatically set to + True and returned appropriately based on value of + ``waitflag``. If it is non-blocking, then the value is + actually checked and not set if it is already acquired. This + is all done so that threading.Condition's assert statements + aren't triggered and throw a little fit. + + """ + if waitflag is None or waitflag: + self.locked_status = True + return True + else: + if not self.locked_status: + self.locked_status = True + return True + else: + if timeout > 0: + import time + time.sleep(timeout) + return False + + __enter__ = acquire + + def __exit__(self, typ, val, tb): + self.release() + + def release(self): + """Release the dummy lock.""" + # XXX Perhaps shouldn't actually bother to test? Could lead + # to problems for complex, threaded code. + if not self.locked_status: + raise error + self.locked_status = False + return True + + def locked(self): + return self.locked_status + +# Used to signal that interrupt_main was called in a "thread" +_interrupt = False +# True when not executing in a "thread" +_main = True + +def interrupt_main(): + """Set _interrupt flag to True to have start_new_thread raise + KeyboardInterrupt upon exiting.""" + if _main: + raise KeyboardInterrupt + else: + global _interrupt + _interrupt = True diff --git a/openpype/vendor/python/python_2/functools32/functools32.py b/openpype/vendor/python/python_2/functools32/functools32.py new file mode 100644 index 0000000000..c44551fac0 --- /dev/null +++ b/openpype/vendor/python/python_2/functools32/functools32.py @@ -0,0 +1,423 @@ +"""functools.py - Tools for working with functions and callable objects +""" +# Python module wrapper for _functools C module +# to allow utilities written in Python to be added +# to the functools module. +# Written by Nick Coghlan +# and Raymond Hettinger +# Copyright (C) 2006-2010 Python Software Foundation. +# See C source code for _functools credits/copyright + +__all__ = ['update_wrapper', 'wraps', 'WRAPPER_ASSIGNMENTS', 'WRAPPER_UPDATES', + 'total_ordering', 'cmp_to_key', 'lru_cache', 'reduce', 'partial'] + +from _functools import partial, reduce +from collections import MutableMapping, namedtuple +from .reprlib32 import recursive_repr as _recursive_repr +from weakref import proxy as _proxy +import sys as _sys +try: + from thread import allocate_lock as Lock +except ImportError: + from ._dummy_thread32 import allocate_lock as Lock + +################################################################################ +### OrderedDict +################################################################################ + +class _Link(object): + __slots__ = 'prev', 'next', 'key', '__weakref__' + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as regular dictionaries. + + # The internal self.__map dict maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # The sentinel is in self.__hardroot with a weakref proxy in self.__root. + # The prev links are weakref proxies (to prevent circular references). + # Individual links are kept alive by the hard reference in self.__map. + # Those hard references disappear when a key is deleted from an OrderedDict. + + def __init__(self, *args, **kwds): + '''Initialize an ordered dictionary. The signature is the same as + regular dictionaries, but keyword arguments are not recommended because + their insertion order is arbitrary. + + ''' + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__hardroot = _Link() + self.__root = root = _proxy(self.__hardroot) + root.prev = root.next = root + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, + dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link at the end of the linked list, + # and the inherited dictionary is updated with the new key/value pair. + if key not in self: + self.__map[key] = link = Link() + root = self.__root + last = root.prev + link.prev, link.next, link.key = last, root, key + last.next = link + root.prev = proxy(link) + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which gets + # removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link = self.__map.pop(key) + link_prev = link.prev + link_next = link.next + link_prev.next = link_next + link_next.prev = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + # Traverse the linked list in order. + root = self.__root + curr = root.next + while curr is not root: + yield curr.key + curr = curr.next + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + # Traverse the linked list in reverse order. + root = self.__root + curr = root.prev + while curr is not root: + yield curr.key + curr = curr.prev + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + root = self.__root + root.prev = root.next = root + self.__map.clear() + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root.prev + link_prev = link.prev + link_prev.next = root + root.prev = link_prev + else: + link = root.next + link_next = link.next + root.next = link_next + link_next.prev = root + key = link.key + del self.__map[key] + value = dict.pop(self, key) + return key, value + + def move_to_end(self, key, last=True): + '''Move an existing element to the end (or beginning if last==False). + + Raises KeyError if the element does not exist. + When last=True, acts like a fast version of self[key]=self.pop(key). + + ''' + link = self.__map[key] + link_prev = link.prev + link_next = link.next + link_prev.next = link_next + link_next.prev = link_prev + root = self.__root + if last: + last = root.prev + link.prev = last + link.next = root + last.next = root.prev = link + else: + first = root.next + link.prev = root + link.next = first + root.next = first.prev = link + + def __sizeof__(self): + sizeof = _sys.getsizeof + n = len(self) + 1 # number of links including root + size = sizeof(self.__dict__) # instance dictionary + size += sizeof(self.__map) * 2 # internal dict and inherited dict + size += sizeof(self.__hardroot) * n # link objects + size += sizeof(self.__root) * n # proxy objects + return size + + update = __update = MutableMapping.update + keys = MutableMapping.keys + values = MutableMapping.values + items = MutableMapping.items + __ne__ = MutableMapping.__ne__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding + value. If key is not found, d is returned if given, otherwise KeyError + is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + @_recursive_repr() + def __repr__(self): + 'od.__repr__() <==> repr(od)' + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self.items())) + + def __reduce__(self): + 'Return state information for pickling' + items = [[k, self[k]] for k in self] + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S. + If not specified, the value defaults to None. + + ''' + self = cls() + for key in iterable: + self[key] = value + return self + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return len(self)==len(other) and \ + all(p==q for p, q in zip(self.items(), other.items())) + return dict.__eq__(self, other) + +# update_wrapper() and wraps() are tools to help write +# wrapper functions that can handle naive introspection + +WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__') +WRAPPER_UPDATES = ('__dict__',) +def update_wrapper(wrapper, + wrapped, + assigned = WRAPPER_ASSIGNMENTS, + updated = WRAPPER_UPDATES): + """Update a wrapper function to look like the wrapped function + + wrapper is the function to be updated + wrapped is the original function + assigned is a tuple naming the attributes assigned directly + from the wrapped function to the wrapper function (defaults to + functools.WRAPPER_ASSIGNMENTS) + updated is a tuple naming the attributes of the wrapper that + are updated with the corresponding attribute from the wrapped + function (defaults to functools.WRAPPER_UPDATES) + """ + wrapper.__wrapped__ = wrapped + for attr in assigned: + try: + value = getattr(wrapped, attr) + except AttributeError: + pass + else: + setattr(wrapper, attr, value) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, {})) + # Return the wrapper so this can be used as a decorator via partial() + return wrapper + +def wraps(wrapped, + assigned = WRAPPER_ASSIGNMENTS, + updated = WRAPPER_UPDATES): + """Decorator factory to apply update_wrapper() to a wrapper function + + Returns a decorator that invokes update_wrapper() with the decorated + function as the wrapper argument and the arguments to wraps() as the + remaining arguments. Default arguments are as for update_wrapper(). + This is a convenience function to simplify applying partial() to + update_wrapper(). + """ + return partial(update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + +def total_ordering(cls): + """Class decorator that fills in missing ordering methods""" + convert = { + '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)), + ('__le__', lambda self, other: self < other or self == other), + ('__ge__', lambda self, other: not self < other)], + '__le__': [('__ge__', lambda self, other: not self <= other or self == other), + ('__lt__', lambda self, other: self <= other and not self == other), + ('__gt__', lambda self, other: not self <= other)], + '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)), + ('__ge__', lambda self, other: self > other or self == other), + ('__le__', lambda self, other: not self > other)], + '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other), + ('__gt__', lambda self, other: self >= other and not self == other), + ('__lt__', lambda self, other: not self >= other)] + } + roots = set(dir(cls)) & set(convert) + if not roots: + raise ValueError('must define at least one ordering operation: < > <= >=') + root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ + for opname, opfunc in convert[root]: + if opname not in roots: + opfunc.__name__ = opname + opfunc.__doc__ = getattr(int, opname).__doc__ + setattr(cls, opname, opfunc) + return cls + +def cmp_to_key(mycmp): + """Convert a cmp= function into a key= function""" + class K(object): + __slots__ = ['obj'] + def __init__(self, obj): + self.obj = obj + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 + __hash__ = None + return K + +_CacheInfo = namedtuple("CacheInfo", "hits misses maxsize currsize") + +def lru_cache(maxsize=100): + """Least-recently-used cache decorator. + + If *maxsize* is set to None, the LRU features are disabled and the cache + can grow without bound. + + Arguments to the cached function must be hashable. + + View the cache statistics named tuple (hits, misses, maxsize, currsize) with + f.cache_info(). Clear the cache and statistics with f.cache_clear(). + Access the underlying function with f.__wrapped__. + + See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used + + """ + # Users should only access the lru_cache through its public API: + # cache_info, cache_clear, and f.__wrapped__ + # The internals of the lru_cache are encapsulated for thread safety and + # to allow the implementation to change (including a possible C version). + + def decorating_function(user_function, + tuple=tuple, sorted=sorted, len=len, KeyError=KeyError): + + hits, misses = [0], [0] + kwd_mark = (object(),) # separates positional and keyword args + lock = Lock() # needed because OrderedDict isn't threadsafe + + if maxsize is None: + cache = dict() # simple cache without ordering or size limit + + @wraps(user_function) + def wrapper(*args, **kwds): + key = args + if kwds: + key += kwd_mark + tuple(sorted(kwds.items())) + try: + result = cache[key] + hits[0] += 1 + return result + except KeyError: + pass + result = user_function(*args, **kwds) + cache[key] = result + misses[0] += 1 + return result + else: + cache = OrderedDict() # ordered least recent to most recent + cache_popitem = cache.popitem + cache_renew = cache.move_to_end + + @wraps(user_function) + def wrapper(*args, **kwds): + key = args + if kwds: + key += kwd_mark + tuple(sorted(kwds.items())) + with lock: + try: + result = cache[key] + cache_renew(key) # record recent use of this key + hits[0] += 1 + return result + except KeyError: + pass + result = user_function(*args, **kwds) + with lock: + cache[key] = result # record recent use of this key + misses[0] += 1 + if len(cache) > maxsize: + cache_popitem(0) # purge least recently used cache entry + return result + + def cache_info(): + """Report cache statistics""" + with lock: + return _CacheInfo(hits[0], misses[0], maxsize, len(cache)) + + def cache_clear(): + """Clear the cache and cache statistics""" + with lock: + cache.clear() + hits[0] = misses[0] = 0 + + wrapper.cache_info = cache_info + wrapper.cache_clear = cache_clear + return wrapper + + return decorating_function diff --git a/openpype/vendor/python/python_2/functools32/reprlib32.py b/openpype/vendor/python/python_2/functools32/reprlib32.py new file mode 100644 index 0000000000..af919758ca --- /dev/null +++ b/openpype/vendor/python/python_2/functools32/reprlib32.py @@ -0,0 +1,157 @@ +"""Redo the builtin repr() (representation) but with limits on most sizes.""" + +__all__ = ["Repr", "repr", "recursive_repr"] + +import __builtin__ as builtins +from itertools import islice +try: + from thread import get_ident +except ImportError: + from _dummy_thread32 import get_ident + +def recursive_repr(fillvalue='...'): + 'Decorator to make a repr function return fillvalue for a recursive call' + + def decorating_function(user_function): + repr_running = set() + + def wrapper(self): + key = id(self), get_ident() + if key in repr_running: + return fillvalue + repr_running.add(key) + try: + result = user_function(self) + finally: + repr_running.discard(key) + return result + + # Can't use functools.wraps() here because of bootstrap issues + wrapper.__module__ = getattr(user_function, '__module__') + wrapper.__doc__ = getattr(user_function, '__doc__') + wrapper.__name__ = getattr(user_function, '__name__') + wrapper.__annotations__ = getattr(user_function, '__annotations__', {}) + return wrapper + + return decorating_function + +class Repr: + + def __init__(self): + self.maxlevel = 6 + self.maxtuple = 6 + self.maxlist = 6 + self.maxarray = 5 + self.maxdict = 4 + self.maxset = 6 + self.maxfrozenset = 6 + self.maxdeque = 6 + self.maxstring = 30 + self.maxlong = 40 + self.maxother = 30 + + def repr(self, x): + return self.repr1(x, self.maxlevel) + + def repr1(self, x, level): + typename = type(x).__name__ + if ' ' in typename: + parts = typename.split() + typename = '_'.join(parts) + if hasattr(self, 'repr_' + typename): + return getattr(self, 'repr_' + typename)(x, level) + else: + return self.repr_instance(x, level) + + def _repr_iterable(self, x, level, left, right, maxiter, trail=''): + n = len(x) + if level <= 0 and n: + s = '...' + else: + newlevel = level - 1 + repr1 = self.repr1 + pieces = [repr1(elem, newlevel) for elem in islice(x, maxiter)] + if n > maxiter: pieces.append('...') + s = ', '.join(pieces) + if n == 1 and trail: right = trail + right + return '%s%s%s' % (left, s, right) + + def repr_tuple(self, x, level): + return self._repr_iterable(x, level, '(', ')', self.maxtuple, ',') + + def repr_list(self, x, level): + return self._repr_iterable(x, level, '[', ']', self.maxlist) + + def repr_array(self, x, level): + header = "array('%s', [" % x.typecode + return self._repr_iterable(x, level, header, '])', self.maxarray) + + def repr_set(self, x, level): + x = _possibly_sorted(x) + return self._repr_iterable(x, level, 'set([', '])', self.maxset) + + def repr_frozenset(self, x, level): + x = _possibly_sorted(x) + return self._repr_iterable(x, level, 'frozenset([', '])', + self.maxfrozenset) + + def repr_deque(self, x, level): + return self._repr_iterable(x, level, 'deque([', '])', self.maxdeque) + + def repr_dict(self, x, level): + n = len(x) + if n == 0: return '{}' + if level <= 0: return '{...}' + newlevel = level - 1 + repr1 = self.repr1 + pieces = [] + for key in islice(_possibly_sorted(x), self.maxdict): + keyrepr = repr1(key, newlevel) + valrepr = repr1(x[key], newlevel) + pieces.append('%s: %s' % (keyrepr, valrepr)) + if n > self.maxdict: pieces.append('...') + s = ', '.join(pieces) + return '{%s}' % (s,) + + def repr_str(self, x, level): + s = builtins.repr(x[:self.maxstring]) + if len(s) > self.maxstring: + i = max(0, (self.maxstring-3)//2) + j = max(0, self.maxstring-3-i) + s = builtins.repr(x[:i] + x[len(x)-j:]) + s = s[:i] + '...' + s[len(s)-j:] + return s + + def repr_int(self, x, level): + s = builtins.repr(x) # XXX Hope this isn't too slow... + if len(s) > self.maxlong: + i = max(0, (self.maxlong-3)//2) + j = max(0, self.maxlong-3-i) + s = s[:i] + '...' + s[len(s)-j:] + return s + + def repr_instance(self, x, level): + try: + s = builtins.repr(x) + # Bugs in x.__repr__() can cause arbitrary + # exceptions -- then make up something + except Exception: + return '<%s instance at %x>' % (x.__class__.__name__, id(x)) + if len(s) > self.maxother: + i = max(0, (self.maxother-3)//2) + j = max(0, self.maxother-3-i) + s = s[:i] + '...' + s[len(s)-j:] + return s + + +def _possibly_sorted(x): + # Since not all sequences of items can be sorted and comparison + # functions may raise arbitrary exceptions, return an unsorted + # sequence in that case. + try: + return sorted(x) + except Exception: + return list(x) + +aRepr = Repr() +repr = aRepr.repr diff --git a/openpype/version.py b/openpype/version.py index d977e87243..55ac148ed1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.9.0-nightly.6" +__version__ = "3.9.0-nightly.7" diff --git a/poetry.lock b/poetry.lock index b6eba33e0a..ee7b839b8d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -674,7 +674,7 @@ ansicon = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "jsonschema" -version = "3.2.0" +version = "2.6.0" description = "An implementation of JSON Schema validation for Python" category = "main" optional = false @@ -1219,6 +1219,26 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "qtawesome" +version = "0.7.3" +description = "FontAwesome icons in PyQt and PySide applications" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +qtpy = "*" +six = "*" + +[[package]] +name = "qtpy" +version = "1.11.3" +description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5, PyQt4 and PySide) and additional custom QWidgets." +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*" + [[package]] name = "recommonmark" version = "0.7.1" @@ -2101,8 +2121,8 @@ jinxed = [ {file = "jinxed-1.1.0.tar.gz", hash = "sha256:d8f1731f134e9e6b04d95095845ae6c10eb15cb223a5f0cabdea87d4a279c305"}, ] jsonschema = [ - {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, - {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, + {file = "jsonschema-2.6.0-py2.py3-none-any.whl", hash = "sha256:000e68abd33c972a5248544925a0cae7d1125f9bf6c58280d37546b946769a08"}, + {file = "jsonschema-2.6.0.tar.gz", hash = "sha256:6ff5f3180870836cae40f06fa10419f557208175f13ad7bc26caa77beb1f6e02"}, ] keyring = [ {file = "keyring-22.4.0-py3-none-any.whl", hash = "sha256:d6c531f6d12f3304db6029af1d19894bd446ecbbadd22465fa0f096b3e12d258"}, @@ -2651,6 +2671,14 @@ pywin32-ctypes = [ {file = "Qt.py-1.3.6-py2.py3-none-any.whl", hash = "sha256:7edf6048d07a6924707506b5ba34a6e05d66dde9a3f4e3a62f9996ccab0b91c7"}, {file = "Qt.py-1.3.6.tar.gz", hash = "sha256:0d78656a2f814602eee304521c7bf5da0cec414818b3833712c77524294c404a"}, ] +qtawesome = [ + {file = "QtAwesome-0.7.3-py2.py3-none-any.whl", hash = "sha256:ddf4530b4af71cec13b24b88a4cdb56ec85b1e44c43c42d0698804c7137b09b0"}, + {file = "QtAwesome-0.7.3.tar.gz", hash = "sha256:b98b9038d19190e83ab26d91c4d8fc3a36591ee2bc7f5016d4438b8240d097bd"}, +] +qtpy = [ + {file = "QtPy-1.11.3-py2.py3-none-any.whl", hash = "sha256:e121fbee8e95645af29c5a4aceba8d657991551fc1aa3b6b6012faf4725a1d20"}, + {file = "QtPy-1.11.3.tar.gz", hash = "sha256:d427addd37386a8d786db81864a5536700861d95bf085cb31d1bea855d699557"}, +] recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, diff --git a/pyproject.toml b/pyproject.toml index 2469cb76a9..f0d295a44c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.9.0-nightly.6" # OpenPype +version = "3.9.0-nightly.7" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" @@ -41,7 +41,7 @@ Click = "^7" dnspython = "^2.1.0" ftrack-python-api = "2.0.*" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) -jsonschema = "^3.2.0" +jsonschema = "^2.6.0" keyring = "^22.0.1" log4mongo = "^1.7" pathlib2= "^2.3.5" # deadline submit publish job only (single place, maybe not needed?) @@ -50,6 +50,8 @@ pyblish-base = "^1.8.8" pynput = "^1.7.2" # idle manager in tray pymongo = "^3.11.2" "Qt.py" = "^1.3.3" +qtpy = "^1.11.3" +qtawesome = "0.7.3" speedcopy = "^2.1" six = "^1.15" semver = "^2.13.0" # for version resolution