diff --git a/Dockerfile.centos7 b/Dockerfile.centos7 index f3b257e66b..736a42663c 100644 --- a/Dockerfile.centos7 +++ b/Dockerfile.centos7 @@ -41,6 +41,8 @@ RUN yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n ncurses \ ncurses-devel \ qt5-qtbase-devel \ + xcb-util-wm \ + xcb-util-renderutil \ && yum clean all # we need to build our own patchelf @@ -92,7 +94,8 @@ RUN source $HOME/.bashrc \ RUN cp /usr/lib64/libffi* ./build/exe.linux-x86_64-3.7/lib \ && cp /usr/lib64/libssl* ./build/exe.linux-x86_64-3.7/lib \ && cp /usr/lib64/libcrypto* ./build/exe.linux-x86_64-3.7/lib \ - && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib + && cp /root/.pyenv/versions/${OPENPYPE_PYTHON_VERSION}/lib/libpython* ./build/exe.linux-x86_64-3.7/lib \ + && cp /usr/lib64/libxcb* ./build/exe.linux-x86_64-3.7/vendor/python/PySide2/Qt/lib RUN cd /opt/openpype \ rm -rf ./vendor/bin diff --git a/openpype/cli.py b/openpype/cli.py index 4c4dc1a3c6..6b20fb5203 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -356,9 +356,22 @@ def run(script): "--pyargs", help="Run tests from package", default=None) -def runtests(folder, mark, pyargs): +@click.option("-t", + "--test_data_folder", + help="Unzipped directory path of test file", + default=None) +@click.option("-s", + "--persist", + help="Persist test DB and published files after test end", + default=None) +@click.option("-a", + "--app_variant", + help="Provide specific app variant for test, empty for latest", + default=None) +def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant): """Run all automatic tests after proper initialization via start.py""" - PypeCommands().run_tests(folder, mark, pyargs) + PypeCommands().run_tests(folder, mark, pyargs, test_data_folder, + persist, app_variant) @main.command() diff --git a/openpype/hosts/aftereffects/plugins/publish/closeAE.py b/openpype/hosts/aftereffects/plugins/publish/closeAE.py new file mode 100644 index 0000000000..21bedf0125 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/closeAE.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +"""Close AE after publish. For Webpublishing only.""" +import pyblish.api + +from avalon import aftereffects + + +class CloseAE(pyblish.api.ContextPlugin): + """Close AE after publish. For Webpublishing only. + """ + + order = pyblish.api.IntegratorOrder + 14 + label = "Close AE" + optional = True + active = True + + hosts = ["aftereffects"] + targets = ["remotepublish"] + + def process(self, context): + self.log.info("CloseAE") + + stub = aftereffects.stub() + self.log.info("Shutting down AE") + stub.save() + stub.close() + self.log.info("AE closed") diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_current_file.py b/openpype/hosts/aftereffects/plugins/publish/collect_current_file.py index b59ff41a0e..51f6f5c844 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_current_file.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_current_file.py @@ -8,7 +8,7 @@ from avalon import aftereffects class CollectCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.49 label = "Current File" hosts = ["aftereffects"] diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py b/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py new file mode 100644 index 0000000000..4e74252043 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/collect_extension_version.py @@ -0,0 +1,56 @@ +import os +import re +import pyblish.api + +from avalon import aftereffects + + +class CollectExtensionVersion(pyblish.api.ContextPlugin): + """ Pulls and compares version of installed extension. + + It is recommended to use same extension as in provided Openpype code. + + Please use Anastasiy’s Extension Manager or ZXPInstaller to update + extension in case of an error. + + You can locate extension.zxp in your installed Openpype code in + `repos/avalon-core/avalon/aftereffects` + """ + # This technically should be a validator, but other collectors might be + # impacted with usage of obsolete extension, so collector that runs first + # was chosen + order = pyblish.api.CollectorOrder - 0.5 + label = "Collect extension version" + hosts = ["aftereffects"] + + optional = True + active = True + + def process(self, context): + installed_version = aftereffects.stub().get_extension_version() + + if not installed_version: + raise ValueError("Unknown version, probably old extension") + + manifest_url = os.path.join(os.path.dirname(aftereffects.__file__), + "extension", "CSXS", "manifest.xml") + + if not os.path.exists(manifest_url): + self.log.debug("Unable to locate extension manifest, not checking") + return + + expected_version = None + with open(manifest_url) as fp: + content = fp.read() + found = re.findall(r'(ExtensionBundleVersion=")([0-9\.]+)(")', + content) + if found: + expected_version = found[0][1] + + if expected_version != installed_version: + msg = ( + "Expected version '{}' found '{}'\n Please update" + " your installed extension, it might not work properly." + ).format(expected_version, installed_version) + + raise ValueError(msg) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index 37337e7fee..b36ab24bde 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -19,10 +19,9 @@ class ExtractLocalRender(openpype.api.Extractor): staging_dir = instance.data["stagingDir"] self.log.info("staging_dir::{}".format(staging_dir)) - stub.render(staging_dir) - # pull file name from Render Queue Output module render_q = stub.get_render_info() + stub.render(staging_dir) if not render_q: raise ValueError("No file extension set in Render Queue") _, ext = os.path.splitext(os.path.basename(render_q.file_name)) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index 6d253300d9..e2a419c8ef 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -32,7 +32,7 @@ class InstallPySideToBlender(PreLaunchHook): def inner_execute(self): # Get blender's python directory - version_regex = re.compile(r"^2\.[0-9]{2}$") + version_regex = re.compile(r"^[2-3]\.[0-9]+$") executable = self.launch_context.executable.executable_path if os.path.basename(executable).lower() != "blender.exe": diff --git a/openpype/hosts/flame/scripts/wiretap_com.py b/openpype/hosts/flame/api/scripts/wiretap_com.py similarity index 100% rename from openpype/hosts/flame/scripts/wiretap_com.py rename to openpype/hosts/flame/api/scripts/wiretap_com.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/__init__.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/app_utils.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/ftrack_lib.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/panel_app.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/modules/uiwidgets.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py rename to openpype/hosts/flame/api/utility_scripts/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py diff --git a/openpype/hosts/flame/utility_scripts/openpype_in_flame.py b/openpype/hosts/flame/api/utility_scripts/openpype_in_flame.py similarity index 100% rename from openpype/hosts/flame/utility_scripts/openpype_in_flame.py rename to openpype/hosts/flame/api/utility_scripts/openpype_in_flame.py diff --git a/openpype/hosts/flame/api/utils.py b/openpype/hosts/flame/api/utils.py index a750046362..201c7d2fac 100644 --- a/openpype/hosts/flame/api/utils.py +++ b/openpype/hosts/flame/api/utils.py @@ -27,6 +27,7 @@ def _sync_utility_scripts(env=None): fsd_paths = [os.path.join( HOST_DIR, + "api", "utility_scripts" )] diff --git a/openpype/hosts/flame/hooks/pre_flame_setup.py b/openpype/hosts/flame/hooks/pre_flame_setup.py index 718c4b574c..159fb37410 100644 --- a/openpype/hosts/flame/hooks/pre_flame_setup.py +++ b/openpype/hosts/flame/hooks/pre_flame_setup.py @@ -22,7 +22,7 @@ class FlamePrelaunch(PreLaunchHook): flame_python_exe = "/opt/Autodesk/python/2021/bin/python2.7" wtc_script_path = os.path.join( - opflame.HOST_DIR, "scripts", "wiretap_com.py") + opflame.HOST_DIR, "api", "scripts", "wiretap_com.py") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/openpype/hosts/maya/plugins/load/load_image_plane.py b/openpype/hosts/maya/plugins/load/load_image_plane.py index f2640dc2eb..eea5844e8b 100644 --- a/openpype/hosts/maya/plugins/load/load_image_plane.py +++ b/openpype/hosts/maya/plugins/load/load_image_plane.py @@ -13,10 +13,14 @@ class CameraWindow(QtWidgets.QDialog): self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint) self.camera = None + self.static_image_plane = False + self.show_in_all_views = False self.widgets = { "label": QtWidgets.QLabel("Select camera for image plane."), "list": QtWidgets.QListWidget(), + "staticImagePlane": QtWidgets.QCheckBox(), + "showInAllViews": QtWidgets.QCheckBox(), "warning": QtWidgets.QLabel("No cameras selected!"), "buttons": QtWidgets.QWidget(), "okButton": QtWidgets.QPushButton("Ok"), @@ -31,6 +35,9 @@ class CameraWindow(QtWidgets.QDialog): for camera in cameras: self.widgets["list"].addItem(camera) + self.widgets["staticImagePlane"].setText("Make Image Plane Static") + self.widgets["showInAllViews"].setText("Show Image Plane in All Views") + # Build buttons. layout = QtWidgets.QHBoxLayout(self.widgets["buttons"]) layout.addWidget(self.widgets["okButton"]) @@ -40,6 +47,8 @@ class CameraWindow(QtWidgets.QDialog): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(self.widgets["label"]) layout.addWidget(self.widgets["list"]) + layout.addWidget(self.widgets["staticImagePlane"]) + layout.addWidget(self.widgets["showInAllViews"]) layout.addWidget(self.widgets["buttons"]) layout.addWidget(self.widgets["warning"]) @@ -54,6 +63,8 @@ class CameraWindow(QtWidgets.QDialog): if self.camera is None: self.widgets["warning"].setVisible(True) return + self.show_in_all_views = self.widgets["showInAllViews"].isChecked() + self.static_image_plane = self.widgets["staticImagePlane"].isChecked() self.close() @@ -65,15 +76,15 @@ class CameraWindow(QtWidgets.QDialog): class ImagePlaneLoader(api.Loader): """Specific loader of plate for image planes on selected camera.""" - families = ["plate", "render"] + families = ["image", "plate", "render"] label = "Load imagePlane." representations = ["mov", "exr", "preview", "png"] icon = "image" color = "orange" - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, data, options=None): import pymel.core as pm - + new_nodes = [] image_plane_depth = 1000 asset = context['asset']['name'] @@ -85,17 +96,23 @@ class ImagePlaneLoader(api.Loader): # Get camera from user selection. camera = None - default_cameras = [ - "frontShape", "perspShape", "sideShape", "topShape" - ] - cameras = [ - x for x in pm.ls(type="camera") if x.name() not in default_cameras - ] - camera_names = {x.getParent().name(): x for x in cameras} - camera_names["Create new camera."] = "create_camera" - window = CameraWindow(camera_names.keys()) - window.exec_() - camera = camera_names[window.camera] + is_static_image_plane = None + is_in_all_views = None + if data: + camera = pm.PyNode(data.get("camera")) + is_static_image_plane = data.get("static_image_plane") + is_in_all_views = data.get("in_all_views") + + if not camera: + cameras = pm.ls(type="camera") + camera_names = {x.getParent().name(): x for x in cameras} + camera_names["Create new camera."] = "create_camera" + window = CameraWindow(camera_names.keys()) + window.exec_() + camera = camera_names[window.camera] + + is_static_image_plane = window.static_image_plane + is_in_all_views = window.show_in_all_views if camera == "create_camera": camera = pm.createNode("camera") @@ -111,13 +128,14 @@ class ImagePlaneLoader(api.Loader): # Create image plane image_plane_transform, image_plane_shape = pm.imagePlane( - camera=camera, showInAllViews=False + fileName=context["representation"]["data"]["path"], + camera=camera, showInAllViews=is_in_all_views ) image_plane_shape.depth.set(image_plane_depth) - image_plane_shape.imageName.set( - context["representation"]["data"]["path"] - ) + if is_static_image_plane: + image_plane_shape.detach() + image_plane_transform.setRotation(camera.getRotation()) start_frame = pm.playbackOptions(q=True, min=True) end_frame = pm.playbackOptions(q=True, max=True) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 53897b21f6..d39750e917 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -492,6 +492,8 @@ class CollectLook(pyblish.api.InstancePlugin): if not cmds.attributeQuery(attr, node=node, exists=True): continue attribute = "{}.{}".format(node, attr) + if cmds.getAttr(attribute, type=True) == "message": + continue node_attributes[attr] = cmds.getAttr(attribute) attributes.append({"name": node, diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 345f5264b7..ac1e495f08 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -224,14 +224,19 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # append full path full_exp_files = [] aov_dict = {} - + default_render_file = context.data.get('project_settings')\ + .get('maya')\ + .get('create')\ + .get('CreateRender')\ + .get('default_render_image_folder') # replace relative paths with absolute. Render products are # returned as list of dictionaries. publish_meta_path = None for aov in exp_files: full_paths = [] for file in aov[aov.keys()[0]]: - full_path = os.path.join(workspace, "renders", file) + full_path = os.path.join(workspace, default_render_file, + file) full_path = full_path.replace("\\", "/") full_paths.append(full_path) publish_meta_path = os.path.dirname(full_path) diff --git a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py index dad1691149..642ca9e25d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py +++ b/openpype/hosts/maya/plugins/publish/validate_render_image_rule.py @@ -23,11 +23,24 @@ class ValidateRenderImageRule(pyblish.api.InstancePlugin): def process(self, instance): - assert get_file_rule("images") == "renders", ( - "Workspace's `images` file rule must be set to: renders" + default_render_file = self.get_default_render_image_folder(instance) + + assert get_file_rule("images") == default_render_file, ( + "Workspace's `images` file rule must be set to: {}".format( + default_render_file + ) ) @classmethod def repair(cls, instance): - pm.workspace.fileRules["images"] = "renders" + default = cls.get_default_render_image_folder(instance) + pm.workspace.fileRules["images"] = default pm.system.Workspace.save() + + @staticmethod + def get_default_render_image_folder(instance): + return instance.context.data.get('project_settings')\ + .get('maya') \ + .get('create') \ + .get('CreateRender') \ + .get('default_render_image_folder') diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index bc7b41c733..50a5d01483 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -42,10 +42,14 @@ class NukeRenderLocal(openpype.api.Extractor): self.log.info("Start frame: {}".format(first_frame)) self.log.info("End frame: {}".format(last_frame)) + # write node url might contain nuke's ctl expressin + # as [python ...]/path... + path = node["file"].evaluate() + # Ensure output directory exists. - directory = os.path.dirname(node["file"].value()) - if not os.path.exists(directory): - os.makedirs(directory) + out_dir = os.path.dirname(path) + if not os.path.exists(out_dir): + os.makedirs(out_dir) # Render frames nuke.execute( @@ -58,15 +62,12 @@ class NukeRenderLocal(openpype.api.Extractor): if "slate" in families: first_frame += 1 - path = node['file'].value() - out_dir = os.path.dirname(path) ext = node["file_type"].value() if "representations" not in instance.data: instance.data["representations"] = [] collected_frames = os.listdir(out_dir) - if len(collected_frames) == 1: repre = { 'name': ext, diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 29faf867d2..af5e8e9d27 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -67,7 +67,9 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): if not repre.get("files"): msg = ("no frames were collected, " - "you need to render them") + "you need to render them.\n" + "Check properties of write node (group) and" + "select 'Local' option in 'Publish' dropdown.") self.log.error(msg) raise ValidationException(msg) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py index 3cc3e3f636..4d4829555e 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_current_file.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_current_file.py @@ -8,7 +8,7 @@ from avalon import photoshop class CollectCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file into context""" - order = pyblish.api.CollectorOrder - 0.5 + order = pyblish.api.CollectorOrder - 0.49 label = "Current File" hosts = ["photoshop"] diff --git a/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py new file mode 100644 index 0000000000..f07ff0b0ff --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_extension_version.py @@ -0,0 +1,57 @@ +import os +import re +import pyblish.api + +from avalon import photoshop + + +class CollectExtensionVersion(pyblish.api.ContextPlugin): + """ Pulls and compares version of installed extension. + + It is recommended to use same extension as in provided Openpype code. + + Please use Anastasiy’s Extension Manager or ZXPInstaller to update + extension in case of an error. + + You can locate extension.zxp in your installed Openpype code in + `repos/avalon-core/avalon/photoshop` + """ + # This technically should be a validator, but other collectors might be + # impacted with usage of obsolete extension, so collector that runs first + # was chosen + order = pyblish.api.CollectorOrder - 0.5 + label = "Collect extension version" + hosts = ["photoshop"] + + optional = True + active = True + + def process(self, context): + installed_version = photoshop.stub().get_extension_version() + + if not installed_version: + raise ValueError("Unknown version, probably old extension") + + manifest_url = os.path.join(os.path.dirname(photoshop.__file__), + "extension", "CSXS", "manifest.xml") + + if not os.path.exists(manifest_url): + self.log.debug("Unable to locate extension manifest, not checking") + return + + expected_version = None + with open(manifest_url) as fp: + content = fp.read() + + found = re.findall(r'(ExtensionBundleVersion=")([0-10\.]+)(")', + content) + if found: + expected_version = found[0][1] + + if expected_version != installed_version: + msg = "Expected version '{}' found '{}'\n".format( + expected_version, installed_version) + msg += "Please update your installed extension, it might not work " + msg += "properly." + + raise ValueError(msg) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 30be92e886..6eb44a9694 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -716,6 +716,8 @@ class ApplicationLaunchContext: # subprocess.Popen launch arguments (first argument in constructor) self.launch_args = executable.as_args() self.launch_args.extend(application.arguments) + if self.data.get("app_args"): + self.launch_args.extend(self.data.pop("app_args")) # Handle launch environemtns env = self.data.pop("env", None) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index a8340d7d09..e3bceff275 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -508,13 +508,18 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): Returns: dict: Data prepared for filling workdir template. """ - hierarchy = "/".join(asset_doc["data"]["parents"]) - task_type = asset_doc['data']['tasks'].get(task_name, {}).get('type') project_task_types = project_doc["config"]["tasks"] task_code = project_task_types.get(task_type, {}).get("short_name") + asset_parents = asset_doc["data"]["parents"] + hierarchy = "/".join(asset_parents) + + parent_name = project_doc["name"] + if asset_parents: + parent_name = asset_parents[-1] + data = { "project": { "name": project_doc["name"], @@ -526,6 +531,7 @@ def get_workdir_data(project_doc, asset_doc, task_name, host_name): "short": task_code, }, "asset": asset_doc["name"], + "parent": parent_name, "app": host_name, "user": getpass.getuser(), "hierarchy": hierarchy, diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 6fd0ad0dfe..9bb0231ca7 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -307,7 +307,6 @@ class HostDirmap: mapping = {} if not project_settings["global"]["sync_server"]["enabled"]: - log.debug("Site Sync not enabled") return mapping from openpype.settings.lib import get_site_local_overrides diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index d7db4d1ab9..8074b2d112 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -2,6 +2,7 @@ import os from datetime import datetime import sys from bson.objectid import ObjectId +import collections import pyblish.util import pyblish.api @@ -11,6 +12,25 @@ from openpype.lib.mongo import OpenPypeMongoConnection from openpype.lib.plugin_tools import parse_json +def headless_publish(log, close_plugin_name=None, is_test=False): + """Runs publish in a opened host with a context and closes Python process. + + Host is being closed via ClosePS pyblish plugin which triggers 'exit' + method in ConsoleTrayApp. + """ + if not is_test: + dbcon = get_webpublish_conn() + _id = os.environ.get("BATCH_LOG_ID") + if not _id: + log.warning("Unable to store log records, " + "batch will be unfinished!") + return + + publish_and_log(dbcon, _id, log, close_plugin_name) + else: + publish(log, close_plugin_name) + + def get_webpublish_conn(): """Get connection to OP 'webpublishes' collection.""" mongo_client = OpenPypeMongoConnection.get_mongo_client() @@ -37,6 +57,33 @@ def start_webpublish_log(dbcon, batch_id, user): }).inserted_id +def publish(log, close_plugin_name=None): + """Loops through all plugins, logs to console. Used for tests. + + Args: + log (OpenPypeLogger) + close_plugin_name (str): name of plugin with responsibility to + close host app + """ + # Error exit as soon as any error occurs. + error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" + + close_plugin = _get_close_plugin(close_plugin_name, log) + + for result in pyblish.util.publish_iter(): + for record in result["records"]: + log.info("{}: {}".format( + result["plugin"].label, record.msg)) + + if result["error"]: + log.error(error_format.format(**result)) + uninstall() + if close_plugin: # close host app explicitly after error + context = pyblish.api.Context() + close_plugin().process(context) + sys.exit(1) + + def publish_and_log(dbcon, _id, log, close_plugin_name=None): """Loops through all plugins, logs ok and fails into OP DB. @@ -140,7 +187,9 @@ def find_variant_key(application_manager, host): found_variant_key = None # finds most up-to-date variant if any installed - for variant_key, variant in app_group.variants.items(): + sorted_variants = collections.OrderedDict( + sorted(app_group.variants.items())) + for variant_key, variant in sorted_variants.items(): for executable in variant.executables: if executable.exists(): found_variant_key = variant_key diff --git a/openpype/modules/default_modules/avalon_apps/__init__.py b/openpype/modules/avalon_apps/__init__.py similarity index 100% rename from openpype/modules/default_modules/avalon_apps/__init__.py rename to openpype/modules/avalon_apps/__init__.py diff --git a/openpype/modules/default_modules/avalon_apps/avalon_app.py b/openpype/modules/avalon_apps/avalon_app.py similarity index 100% rename from openpype/modules/default_modules/avalon_apps/avalon_app.py rename to openpype/modules/avalon_apps/avalon_app.py diff --git a/openpype/modules/default_modules/avalon_apps/rest_api.py b/openpype/modules/avalon_apps/rest_api.py similarity index 100% rename from openpype/modules/default_modules/avalon_apps/rest_api.py rename to openpype/modules/avalon_apps/rest_api.py diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 7ecfeae7bd..a1df3cfd14 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -29,6 +29,21 @@ from openpype.settings.lib import ( from openpype.lib import PypeLogger +DEFAULT_OPENPYPE_MODULES = ( + "avalon_apps", + "clockify", + "log_viewer", + "muster", + "python_console_interpreter", + "slack", + "webserver", + "launcher_action", + "project_manager_action", + "settings_action", + "standalonepublish_action", +) + + # Inherit from `object` for Python 2 hosts class _ModuleClass(object): """Fake module class for storing OpenPype modules. @@ -272,17 +287,12 @@ def _load_modules(): log = PypeLogger.get_logger("ModulesLoader") # Import default modules imported from 'openpype.modules' - for default_module_name in ( - "settings_action", - "launcher_action", - "project_manager_action", - "standalonepublish_action", - ): + for default_module_name in DEFAULT_OPENPYPE_MODULES: try: - default_module = __import__( - "openpype.modules.{}".format(default_module_name), - fromlist=("", ) - ) + import_str = "openpype.modules.{}".format(default_module_name) + new_import_str = "{}.{}".format(modules_key, default_module_name) + default_module = __import__(import_str, fromlist=("", )) + sys.modules[new_import_str] = default_module setattr(openpype_modules, default_module_name, default_module) except Exception: diff --git a/openpype/modules/default_modules/clockify/__init__.py b/openpype/modules/clockify/__init__.py similarity index 100% rename from openpype/modules/default_modules/clockify/__init__.py rename to openpype/modules/clockify/__init__.py diff --git a/openpype/modules/default_modules/clockify/clockify_api.py b/openpype/modules/clockify/clockify_api.py similarity index 100% rename from openpype/modules/default_modules/clockify/clockify_api.py rename to openpype/modules/clockify/clockify_api.py diff --git a/openpype/modules/default_modules/clockify/clockify_module.py b/openpype/modules/clockify/clockify_module.py similarity index 100% rename from openpype/modules/default_modules/clockify/clockify_module.py rename to openpype/modules/clockify/clockify_module.py diff --git a/openpype/modules/default_modules/clockify/constants.py b/openpype/modules/clockify/constants.py similarity index 100% rename from openpype/modules/default_modules/clockify/constants.py rename to openpype/modules/clockify/constants.py diff --git a/openpype/modules/default_modules/clockify/ftrack/server/action_clockify_sync_server.py b/openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py similarity index 100% rename from openpype/modules/default_modules/clockify/ftrack/server/action_clockify_sync_server.py rename to openpype/modules/clockify/ftrack/server/action_clockify_sync_server.py diff --git a/openpype/modules/default_modules/clockify/ftrack/user/action_clockify_sync_local.py b/openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py similarity index 100% rename from openpype/modules/default_modules/clockify/ftrack/user/action_clockify_sync_local.py rename to openpype/modules/clockify/ftrack/user/action_clockify_sync_local.py diff --git a/openpype/modules/default_modules/clockify/launcher_actions/ClockifyStart.py b/openpype/modules/clockify/launcher_actions/ClockifyStart.py similarity index 100% rename from openpype/modules/default_modules/clockify/launcher_actions/ClockifyStart.py rename to openpype/modules/clockify/launcher_actions/ClockifyStart.py diff --git a/openpype/modules/default_modules/clockify/launcher_actions/ClockifySync.py b/openpype/modules/clockify/launcher_actions/ClockifySync.py similarity index 100% rename from openpype/modules/default_modules/clockify/launcher_actions/ClockifySync.py rename to openpype/modules/clockify/launcher_actions/ClockifySync.py diff --git a/openpype/modules/default_modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py similarity index 100% rename from openpype/modules/default_modules/clockify/widgets.py rename to openpype/modules/clockify/widgets.py diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py index e6c42374ca..51a19e2aad 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py @@ -394,9 +394,14 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.log.debug(filepath) # Gather needed data ------------------------------------------------ + default_render_file = instance.context.data.get('project_settings')\ + .get('maya')\ + .get('create')\ + .get('CreateRender')\ + .get('default_render_image_folder') filename = os.path.basename(filepath) comment = context.data.get("comment", "") - dirname = os.path.join(workspace, "renders") + dirname = os.path.join(workspace, default_render_file) renderlayer = instance.data['setMembers'] # rs_beauty deadline_user = context.data.get("user", getpass.getuser()) jobname = "%s - %s" % (filename, instance.name) diff --git a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py index 3ba874281a..f58eb91485 100644 --- a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/default_modules/ftrack/lib/avalon_sync.py @@ -360,6 +360,8 @@ class SyncEntitiesFactory: self._subsets_by_parent_id = None self._changeability_by_mongo_id = None + self._object_types_by_name = None + self.all_filtered_entities = {} self.filtered_ids = [] self.not_selected_ids = [] @@ -651,6 +653,18 @@ class SyncEntitiesFactory: self._bubble_changeability(list(self.subsets_by_parent_id.keys())) return self._changeability_by_mongo_id + @property + def object_types_by_name(self): + if self._object_types_by_name is None: + object_types_by_name = self.session.query( + "select id, name from ObjectType" + ).all() + self._object_types_by_name = { + object_type["name"]: object_type + for object_type in object_types_by_name + } + return self._object_types_by_name + @property def all_ftrack_names(self): """ @@ -880,10 +894,7 @@ class SyncEntitiesFactory: custom_attrs, hier_attrs = get_openpype_attr( self.session, query_keys=self.cust_attr_query_keys ) - ent_types = self.session.query("select id, name from ObjectType").all() - ent_types_by_name = { - ent_type["name"]: ent_type["id"] for ent_type in ent_types - } + ent_types_by_name = self.object_types_by_name # Custom attribute types cust_attr_types = self.session.query( "select id, name from CustomAttributeType" @@ -2491,7 +2502,13 @@ class SyncEntitiesFactory: parent_entity = self.entities_dict[parent_id]["entity"] _name = av_entity["name"] - _type = av_entity["data"].get("entityType", "folder") + _type = av_entity["data"].get("entityType") + # Check existence of object type + if _type and _type not in self.object_types_by_name: + _type = None + + if not _type: + _type = "Folder" self.log.debug(( "Re-ceating deleted entity {} <{}>" diff --git a/openpype/modules/default_modules/log_viewer/__init__.py b/openpype/modules/log_viewer/__init__.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/__init__.py rename to openpype/modules/log_viewer/__init__.py diff --git a/openpype/modules/default_modules/log_viewer/log_view_module.py b/openpype/modules/log_viewer/log_view_module.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/log_view_module.py rename to openpype/modules/log_viewer/log_view_module.py diff --git a/openpype/modules/default_modules/log_viewer/tray/__init__.py b/openpype/modules/log_viewer/tray/__init__.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/tray/__init__.py rename to openpype/modules/log_viewer/tray/__init__.py diff --git a/openpype/modules/default_modules/log_viewer/tray/app.py b/openpype/modules/log_viewer/tray/app.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/tray/app.py rename to openpype/modules/log_viewer/tray/app.py diff --git a/openpype/modules/default_modules/log_viewer/tray/models.py b/openpype/modules/log_viewer/tray/models.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/tray/models.py rename to openpype/modules/log_viewer/tray/models.py diff --git a/openpype/modules/default_modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py similarity index 100% rename from openpype/modules/default_modules/log_viewer/tray/widgets.py rename to openpype/modules/log_viewer/tray/widgets.py diff --git a/openpype/modules/default_modules/muster/__init__.py b/openpype/modules/muster/__init__.py similarity index 100% rename from openpype/modules/default_modules/muster/__init__.py rename to openpype/modules/muster/__init__.py diff --git a/openpype/modules/default_modules/muster/muster.py b/openpype/modules/muster/muster.py similarity index 100% rename from openpype/modules/default_modules/muster/muster.py rename to openpype/modules/muster/muster.py diff --git a/openpype/modules/default_modules/muster/rest_api.py b/openpype/modules/muster/rest_api.py similarity index 100% rename from openpype/modules/default_modules/muster/rest_api.py rename to openpype/modules/muster/rest_api.py diff --git a/openpype/modules/default_modules/muster/widget_login.py b/openpype/modules/muster/widget_login.py similarity index 100% rename from openpype/modules/default_modules/muster/widget_login.py rename to openpype/modules/muster/widget_login.py diff --git a/openpype/modules/default_modules/python_console_interpreter/__init__.py b/openpype/modules/python_console_interpreter/__init__.py similarity index 100% rename from openpype/modules/default_modules/python_console_interpreter/__init__.py rename to openpype/modules/python_console_interpreter/__init__.py diff --git a/openpype/modules/default_modules/python_console_interpreter/module.py b/openpype/modules/python_console_interpreter/module.py similarity index 100% rename from openpype/modules/default_modules/python_console_interpreter/module.py rename to openpype/modules/python_console_interpreter/module.py diff --git a/openpype/modules/default_modules/python_console_interpreter/window/__init__.py b/openpype/modules/python_console_interpreter/window/__init__.py similarity index 100% rename from openpype/modules/default_modules/python_console_interpreter/window/__init__.py rename to openpype/modules/python_console_interpreter/window/__init__.py diff --git a/openpype/modules/default_modules/python_console_interpreter/window/widgets.py b/openpype/modules/python_console_interpreter/window/widgets.py similarity index 100% rename from openpype/modules/default_modules/python_console_interpreter/window/widgets.py rename to openpype/modules/python_console_interpreter/window/widgets.py diff --git a/openpype/modules/default_modules/slack/README.md b/openpype/modules/slack/README.md similarity index 100% rename from openpype/modules/default_modules/slack/README.md rename to openpype/modules/slack/README.md diff --git a/openpype/modules/default_modules/slack/__init__.py b/openpype/modules/slack/__init__.py similarity index 100% rename from openpype/modules/default_modules/slack/__init__.py rename to openpype/modules/slack/__init__.py diff --git a/openpype/modules/default_modules/slack/launch_hooks/pre_python2_vendor.py b/openpype/modules/slack/launch_hooks/pre_python2_vendor.py similarity index 100% rename from openpype/modules/default_modules/slack/launch_hooks/pre_python2_vendor.py rename to openpype/modules/slack/launch_hooks/pre_python2_vendor.py diff --git a/openpype/modules/default_modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml similarity index 100% rename from openpype/modules/default_modules/slack/manifest.yml rename to openpype/modules/slack/manifest.yml diff --git a/openpype/modules/default_modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py similarity index 100% rename from openpype/modules/default_modules/slack/plugins/publish/collect_slack_family.py rename to openpype/modules/slack/plugins/publish/collect_slack_family.py diff --git a/openpype/modules/default_modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py similarity index 100% rename from openpype/modules/default_modules/slack/plugins/publish/integrate_slack_api.py rename to openpype/modules/slack/plugins/publish/integrate_slack_api.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.flake8 b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.flake8 similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.flake8 rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.flake8 diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.gitignore b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.gitignore similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.gitignore rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.gitignore diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/LICENSE b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/LICENSE similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/LICENSE rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/LICENSE diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/README.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/README.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/README.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/README.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/localtoc.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/relations.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs.sh b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs.sh similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs.sh rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs.sh diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/documentation_options.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down-pressed.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/down.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/file.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery-3.2.1.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/jquery.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/language_data.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/minus.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/plus.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/pygments.css diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/searchtools.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/sidebar.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore-1.3.1.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/underscore.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up-pressed.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/up.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/websupport.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/about.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/auth.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/basic_usage.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/changelog.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/conversations.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/faq.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/genindex.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/index.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/metadata.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/objects.inv diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/real_time_messaging.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/search.html diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/searchindex.js diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/requirements.txt diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.cfg diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/setup.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/setup.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/setup.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/__init__.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/channel.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/client.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/exceptions.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/im.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/server.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/slackrequest.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/user.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/util.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/slackclient/version.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/test_requirements.txt diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/conftest.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/channel.created.json diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/im.created.json diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/rtm.start.json diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/data/slack_logo.png diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_channel.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_server.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackclient.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tests/test_slackrequest.py diff --git a/openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tox.ini b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/tox.ini similarity index 100% rename from openpype/modules/default_modules/slack/python2_vendor/python-slack-sdk-1/tox.ini rename to openpype/modules/slack/python2_vendor/python-slack-sdk-1/tox.ini diff --git a/openpype/modules/default_modules/slack/slack_module.py b/openpype/modules/slack/slack_module.py similarity index 100% rename from openpype/modules/default_modules/slack/slack_module.py rename to openpype/modules/slack/slack_module.py diff --git a/openpype/modules/default_modules/webserver/__init__.py b/openpype/modules/webserver/__init__.py similarity index 100% rename from openpype/modules/default_modules/webserver/__init__.py rename to openpype/modules/webserver/__init__.py diff --git a/openpype/modules/default_modules/webserver/base_routes.py b/openpype/modules/webserver/base_routes.py similarity index 100% rename from openpype/modules/default_modules/webserver/base_routes.py rename to openpype/modules/webserver/base_routes.py diff --git a/openpype/modules/default_modules/webserver/host_console_listener.py b/openpype/modules/webserver/host_console_listener.py similarity index 100% rename from openpype/modules/default_modules/webserver/host_console_listener.py rename to openpype/modules/webserver/host_console_listener.py diff --git a/openpype/modules/default_modules/webserver/server.py b/openpype/modules/webserver/server.py similarity index 100% rename from openpype/modules/default_modules/webserver/server.py rename to openpype/modules/webserver/server.py diff --git a/openpype/modules/default_modules/webserver/webserver_module.py b/openpype/modules/webserver/webserver_module.py similarity index 100% rename from openpype/modules/default_modules/webserver/webserver_module.py rename to openpype/modules/webserver/webserver_module.py diff --git a/openpype/plugins/publish/collect_anatomy_context_data.py b/openpype/plugins/publish/collect_anatomy_context_data.py index 6b95979b76..07de1b4420 100644 --- a/openpype/plugins/publish/collect_anatomy_context_data.py +++ b/openpype/plugins/publish/collect_anatomy_context_data.py @@ -49,24 +49,27 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): project_entity = context.data["projectEntity"] asset_entity = context.data["assetEntity"] - hierarchy_items = asset_entity["data"]["parents"] - hierarchy = "" - if hierarchy_items: - hierarchy = os.path.join(*hierarchy_items) - asset_tasks = asset_entity["data"]["tasks"] task_type = asset_tasks.get(task_name, {}).get("type") project_task_types = project_entity["config"]["tasks"] task_code = project_task_types.get(task_type, {}).get("short_name") + asset_parents = asset_entity["data"]["parents"] + hierarchy = "/".join(asset_parents) + + parent_name = project_entity["name"] + if asset_parents: + parent_name = asset_parents[-1] + context_data = { "project": { "name": project_entity["name"], "code": project_entity["data"].get("code") }, "asset": asset_entity["name"], - "hierarchy": hierarchy.replace("\\", "/"), + "parent": parent_name, + "hierarchy": hierarchy, "task": { "name": task_name, "type": task_type, diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index da6a2195ee..74b556e28a 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -242,7 +242,11 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): asset_doc = instance.data.get("assetEntity") if asset_doc and asset_doc["_id"] != context_asset_doc["_id"]: parents = asset_doc["data"].get("parents") or list() + parent_name = project_doc["name"] + if parents: + parent_name = parents[-1] anatomy_updates["hierarchy"] = "/".join(parents) + anatomy_updates["parent"] = parent_name # Task task_name = instance.data.get("task") diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 519e7c285b..a6330bae1f 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -216,6 +216,7 @@ class PypeCommands: task_name, app_name ) + print("env:: {}".format(env)) os.environ.update(env) os.environ["OPENPYPE_PUBLISH_DATA"] = batch_dir @@ -340,7 +341,8 @@ class PypeCommands: def validate_jsons(self): pass - def run_tests(self, folder, mark, pyargs): + def run_tests(self, folder, mark, pyargs, + test_data_folder, persist, app_variant): """ Runs tests from 'folder' @@ -348,25 +350,39 @@ class PypeCommands: folder (str): relative path to folder with tests mark (str): label to run tests marked by it (slow etc) pyargs (str): package path to test + test_data_folder (str): url to unzipped folder of test data + persist (bool): True if keep test db and published after test + end + app_variant (str): variant (eg 2020 for AE), empty if use + latest installed version """ print("run_tests") - import subprocess - if folder: folder = " ".join(list(folder)) else: folder = "../tests" - mark_str = pyargs_str = '' + # disable warnings and show captured stdout even if success + args = ["--disable-pytest-warnings", "-rP", folder] + if mark: - mark_str = "-m {}".format(mark) + args.extend(["-m", mark]) if pyargs: - pyargs_str = "--pyargs {}".format(pyargs) + args.extend(["--pyargs", pyargs]) - cmd = "pytest {} {} {}".format(folder, mark_str, pyargs_str) - print("Running {}".format(cmd)) - subprocess.run(cmd) + if persist: + args.extend(["--test_data_folder", test_data_folder]) + + if persist: + args.extend(["--persist", persist]) + + if app_variant: + args.extend(["--app_variant", app_variant]) + + print("run_tests args: {}".format(args)) + import pytest + pytest.main(args) def syncserver(self, active_site): """Start running sync_server in background.""" diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index f4b9760fe1..b75b0168ec 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -43,7 +43,8 @@ "defaults": [ "Main" ], - "aov_separator": "underscore" + "aov_separator": "underscore", + "default_render_image_folder": "renders" }, "CreateAnimation": { "enabled": true, diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index d536652581..1cbe09f576 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -142,7 +142,7 @@ "icon": "{}/app_icons/nuke.png", "host_name": "nuke", "environment": { - "NUKE_PATH": "{OPENPYPE_STUDIO_PLUGINS}/nuke" + "NUKE_PATH": ["{NUKE_PATH}", "{OPENPYPE_STUDIO_PLUGINS}/nuke"] }, "variants": { "13-0": { @@ -248,7 +248,7 @@ "icon": "{}/app_icons/nuke.png", "host_name": "nuke", "environment": { - "NUKE_PATH": "{OPENPYPE_STUDIO_PLUGINS}/nuke" + "NUKE_PATH": ["{NUKE_PATH}", "{OPENPYPE_STUDIO_PLUGINS}/nuke"] }, "variants": { "13-0": { @@ -1101,6 +1101,23 @@ "linux": [] }, "environment": {} + }, + "2022": { + "enabled": true, + "variant_label": "2022", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2022\\Support Files\\AfterFX.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": [], + "darwin": [], + "linux": [] + }, + "environment": {} } } }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index e50357cc40..088d5d1f96 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -58,6 +58,11 @@ {"underscore": "_ (underscore)"}, {"dot": ". (dot)"} ] + }, + { + "type": "text", + "key": "default_render_image_folder", + "label": "Default render image folder" } ] }, diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index ff75562413..43489aecfd 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -933,8 +933,10 @@ def get_general_environments(): # - prevent to use `get_system_settings` where `get_default_settings` # is used default_values = load_openpype_default_settings() + system_settings = default_values["system_settings"] studio_overrides = get_studio_system_settings_overrides() - result = apply_overrides(default_values, studio_overrides) + + result = apply_overrides(system_settings, studio_overrides) environments = result["general"]["environment"] clear_metadata_from_settings(environments) diff --git a/openpype/style/style.css b/openpype/style/style.css index 19245cdc40..4159fe1676 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1044,16 +1044,45 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { color: {color:settings:label-fg}; } #SettingsLabel:hover {color: {color:settings:label-fg-hover};} -#SettingsLabel[state="studio"] {color: {color:settings:studio-light};} -#SettingsLabel[state="studio"]:hover {color: {color:settings:studio-label-hover};} -#SettingsLabel[state="modified"] {color: {color:settings:modified-mid};} -#SettingsLabel[state="modified"]:hover {color: {color:settings:modified-light};} -#SettingsLabel[state="overriden-modified"] {color: {color:settings:modified-mid};} -#SettingsLabel[state="overriden-modified"]:hover {color: {color:settings:modified-light};} -#SettingsLabel[state="overriden"] {color: {color:settings:project-mid};} -#SettingsLabel[state="overriden"]:hover {color: {color:settings:project-light};} -#SettingsLabel[state="invalid"] {color:{color:settings:invalid-dark};} -#SettingsLabel[state="invalid"]:hover {color: {color:settings:invalid-dark};} + +#ExpandLabel { + font-weight: bold; + color: {color:settings:label-fg}; +} +#ExpandLabel:hover { + color: {color:settings:label-fg-hover}; +} + +#ExpandLabel[state="studio"], #SettingsLabel[state="studio"] { + color: {color:settings:studio-light}; +} +#ExpandLabel[state="studio"]:hover, #SettingsLabel[state="studio"]:hover { + color: {color:settings:studio-label-hover}; +} +#ExpandLabel[state="modified"], #SettingsLabel[state="modified"] { + color: {color:settings:modified-mid}; +} +#ExpandLabel[state="modified"]:hover, #SettingsLabel[state="modified"]:hover { + color: {color:settings:modified-light}; +} +#ExpandLabel[state="overriden-modified"], #SettingsLabel[state="overriden-modified"] { + color: {color:settings:modified-mid}; +} +#ExpandLabel[state="overriden-modified"]:hover, #SettingsLabel[state="overriden-modified"]:hover { + color: {color:settings:modified-light}; +} +#ExpandLabel[state="overriden"], #SettingsLabel[state="overriden"] { + color: {color:settings:project-mid}; +} +#ExpandLabel[state="overriden"]:hover, #SettingsLabel[state="overriden"]:hover { + color: {color:settings:project-light}; +} +#ExpandLabel[state="invalid"], #SettingsLabel[state="invalid"] { + color:{color:settings:invalid-dark}; +} +#ExpandLabel[state="invalid"]:hover, #SettingsLabel[state="invalid"]:hover { + color: {color:settings:invalid-dark}; +} /* TODO Replace these with explicit widget types if possible */ #SettingsMainWidget QWidget[input-state="modified"] { @@ -1085,14 +1114,6 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { #DictKey[state="modified"] {border-color: {color:settings:modified-mid};} #DictKey[state="invalid"] {border-color: {color:settings:invalid-dark};} -#ExpandLabel { - font-weight: bold; - color: {color:settings:label-fg}; -} -#ExpandLabel:hover { - color: {color:settings:label-fg-hover}; -} - #ContentWidget { background-color: transparent; } diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index f8378ed18c..b10c958880 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -1,9 +1,15 @@ +import sys import json +import traceback from Qt import QtWidgets, QtGui, QtCore + +from openpype.settings.entities import ProjectSettings from openpype.tools.settings import CHILD_OFFSET + from .widgets import ExpandingWidget from .lib import create_deffered_value_change_timer +from .constants import DEFAULT_PROJECT_LABEL class BaseWidget(QtWidgets.QWidget): @@ -110,9 +116,10 @@ class BaseWidget(QtWidgets.QWidget): return def discard_changes(): - self.ignore_input_changes.set_ignore(True) - self.entity.discard_changes() - self.ignore_input_changes.set_ignore(False) + with self.category_widget.working_state_context(): + self.ignore_input_changes.set_ignore(True) + self.entity.discard_changes() + self.ignore_input_changes.set_ignore(False) action = QtWidgets.QAction("Discard changes") actions_mapping[action] = discard_changes @@ -124,8 +131,11 @@ class BaseWidget(QtWidgets.QWidget): if not self.entity.can_trigger_add_to_studio_default: return + def add_to_studio_default(): + with self.category_widget.working_state_context(): + self.entity.add_to_studio_default() action = QtWidgets.QAction("Add to studio default") - actions_mapping[action] = self.entity.add_to_studio_default + actions_mapping[action] = add_to_studio_default menu.addAction(action) def _remove_from_studio_default_action(self, menu, actions_mapping): @@ -133,9 +143,10 @@ class BaseWidget(QtWidgets.QWidget): return def remove_from_studio_default(): - self.ignore_input_changes.set_ignore(True) - self.entity.remove_from_studio_default() - self.ignore_input_changes.set_ignore(False) + with self.category_widget.working_state_context(): + self.ignore_input_changes.set_ignore(True) + self.entity.remove_from_studio_default() + self.ignore_input_changes.set_ignore(False) action = QtWidgets.QAction("Remove from studio default") actions_mapping[action] = remove_from_studio_default menu.addAction(action) @@ -144,8 +155,12 @@ class BaseWidget(QtWidgets.QWidget): if not self.entity.can_trigger_add_to_project_override: return + def add_to_project_override(): + with self.category_widget.working_state_context(): + self.entity.add_to_project_override + action = QtWidgets.QAction("Add to project project override") - actions_mapping[action] = self.entity.add_to_project_override + actions_mapping[action] = add_to_project_override menu.addAction(action) def _remove_from_project_override_action(self, menu, actions_mapping): @@ -153,9 +168,11 @@ class BaseWidget(QtWidgets.QWidget): return def remove_from_project_override(): - self.ignore_input_changes.set_ignore(True) - self.entity.remove_from_project_override() - self.ignore_input_changes.set_ignore(False) + with self.category_widget.working_state_context(): + self.ignore_input_changes.set_ignore(True) + self.entity.remove_from_project_override() + self.ignore_input_changes.set_ignore(False) + action = QtWidgets.QAction("Remove from project override") actions_mapping[action] = remove_from_project_override menu.addAction(action) @@ -257,14 +274,16 @@ class BaseWidget(QtWidgets.QWidget): # Simple paste value method def paste_value(): - _set_entity_value(self.entity, value) + with self.category_widget.working_state_context(): + _set_entity_value(self.entity, value) action = QtWidgets.QAction("Paste", menu) output.append((action, paste_value)) # Paste value to matchin entity def paste_value_to_path(): - _set_entity_value(matching_entity, value) + with self.category_widget.working_state_context(): + _set_entity_value(matching_entity, value) if matching_entity is not None: action = QtWidgets.QAction("Paste to same place", menu) @@ -272,6 +291,68 @@ class BaseWidget(QtWidgets.QWidget): return output + def _apply_values_from_project_action(self, menu, actions_mapping): + for attr_name in ("project_name", "get_project_names"): + if not hasattr(self.category_widget, attr_name): + return + + if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: + return + + current_project_name = self.category_widget.project_name + project_names = [] + for project_name in self.category_widget.get_project_names(): + if project_name != current_project_name: + project_names.append(project_name) + + if not project_names: + return + + submenu = QtWidgets.QMenu("Apply values from", menu) + + for project_name in project_names: + if project_name is None: + project_name = DEFAULT_PROJECT_LABEL + + action = QtWidgets.QAction(project_name) + submenu.addAction(action) + actions_mapping[action] = lambda: self._apply_values_from_project( + project_name + ) + menu.addMenu(submenu) + + def _apply_values_from_project(self, project_name): + with self.category_widget.working_state_context(): + try: + path_keys = [ + item + for item in self.entity.path.split("/") + if item + ] + entity = ProjectSettings(project_name) + for key in path_keys: + entity = entity[key] + self.entity.set(entity.value) + + except Exception: + if project_name is None: + project_name = DEFAULT_PROJECT_LABEL + + # TODO better message + title = "Applying values failed" + msg = "Applying values from project \"{}\" failed.".format( + project_name + ) + detail_msg = "".join( + traceback.format_exception(*sys.exc_info()) + ) + dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle(title) + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setText(msg) + dialog.setDetailedText(detail_msg) + dialog.exec_() + def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: return @@ -290,6 +371,7 @@ class BaseWidget(QtWidgets.QWidget): self._remove_from_studio_default_action(menu, actions_mapping) self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) + self._apply_values_from_project_action(menu, actions_mapping) ui_actions = [] ui_actions.extend(self._copy_value_actions(menu)) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index a6e4154b2b..029619849e 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -1,6 +1,7 @@ import os import sys import traceback +import contextlib from enum import Enum from Qt import QtWidgets, QtCore, QtGui @@ -309,6 +310,12 @@ class SettingsCategoryWidget(QtWidgets.QWidget): ) self.content_layout.addWidget(widget, 0) + @contextlib.contextmanager + def working_state_context(self): + self.set_state(CategoryState.Working) + yield + self.set_state(CategoryState.Idle) + def save(self): if not self.items_are_valid(): return @@ -599,6 +606,14 @@ class ProjectWidget(SettingsCategoryWidget): self.project_list_widget = project_list_widget + def get_project_names(self): + if ( + self.modify_defaults_checkbox + and self.modify_defaults_checkbox.isChecked() + ): + return [] + return self.project_list_widget.get_project_names() + def on_saved(self, saved_tab_widget): """Callback on any tab widget save. diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index ac9870287b..4c7bf87ce8 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -747,6 +747,13 @@ class ProjectListWidget(QtWidgets.QWidget): index, QtCore.QItemSelectionModel.SelectionFlag.SelectCurrent ) + def get_project_names(self): + output = [] + for row in range(self.project_proxy.rowCount()): + index = self.project_proxy.index(row, 0) + output.append(index.data(PROJECT_NAME_ROLE)) + return output + def refresh(self): selected_project = None for index in self.project_list.selectedIndexes(): diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 009c1dc506..3bfa092a21 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -12,22 +12,17 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit): """Set placeholder color of QLineEdit in Qt 5.12 and higher.""" def __init__(self, *args, **kwargs): super(PlaceholderLineEdit, self).__init__(*args, **kwargs) - self._first_show = True - - def showEvent(self, event): - super(PlaceholderLineEdit, self).showEvent(event) - if self._first_show: - self._first_show = False + # Change placeholder palette color + if hasattr(QtGui.QPalette, "PlaceholderText"): filter_palette = self.palette() - if hasattr(filter_palette, "PlaceholderText"): - color_obj = get_objected_colors()["font"] - color = color_obj.get_qcolor() - color.setAlpha(67) - filter_palette.setColor( - filter_palette.PlaceholderText, - color - ) - self.setPalette(filter_palette) + color_obj = get_objected_colors()["font"] + color = color_obj.get_qcolor() + color.setAlpha(67) + filter_palette.setColor( + QtGui.QPalette.PlaceholderText, + color + ) + self.setPalette(filter_palette) class ImageButton(QtWidgets.QPushButton): diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index d33294e4ad..7973b88b82 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -69,12 +69,16 @@ class NameWindow(QtWidgets.QDialog): "config.tasks": True, } ) + asset_doc = io.find_one( { "type": "asset", "name": asset_name }, - {"data.tasks": True} + { + "data.tasks": True, + "data.parents": True + } ) task_type = asset_doc["data"]["tasks"].get(task_name, {}).get("type") @@ -82,6 +86,11 @@ class NameWindow(QtWidgets.QDialog): project_task_types = project_doc["config"]["tasks"] task_short = project_task_types.get(task_type, {}).get("short_name") + asset_parents = asset_doc["data"]["parents"] + parent_name = project_doc["name"] + if asset_parents: + parent_name = asset_parents[-1] + self.data = { "project": { "name": project_doc["name"], @@ -93,6 +102,7 @@ class NameWindow(QtWidgets.QDialog): "type": task_type, "short": task_short, }, + "parent": parent_name, "version": 1, "user": getpass.getuser(), "comment": "", diff --git a/start.py b/start.py index 0f7e82071d..01831e9b4d 100644 --- a/start.py +++ b/start.py @@ -339,13 +339,14 @@ def set_avalon_environments(): os.environ.get("AVALON_MONGO") or os.environ["OPENPYPE_MONGO"] ) + avalon_db = os.environ.get("AVALON_DB") or "avalon" # for tests os.environ.update({ # Mongo url (use same as OpenPype has) "AVALON_MONGO": avalon_mongo_url, "AVALON_SCHEMA": schema_path, # Mongo DB name where avalon docs are stored - "AVALON_DB": "avalon", + "AVALON_DB": avalon_db, # Name of config "AVALON_CONFIG": "openpype", "AVALON_LABEL": "OpenPype" @@ -925,7 +926,9 @@ def boot(): sys.exit(1) os.environ["OPENPYPE_MONGO"] = openpype_mongo - os.environ["OPENPYPE_DATABASE_NAME"] = "openpype" # name of Pype database + # name of Pype database + os.environ["OPENPYPE_DATABASE_NAME"] = \ + os.environ.get("OPENPYPE_DATABASE_NAME") or "openpype" _print(">>> run disk mapping command ...") run_disk_mapping_commands(openpype_mongo) diff --git a/tests/README.md b/tests/README.md index 6317b2ab3c..d0578f8059 100644 --- a/tests/README.md +++ b/tests/README.md @@ -14,12 +14,12 @@ How to run: ---------- - single test class could be run by PyCharm and its pytest runner directly - OR -- use Openpype command 'runtests' from command line --- `${OPENPYPE_ROOT}/start.py runtests` +- use Openpype command 'runtests' from command line (`.venv` in ${OPENPYPE_ROOT} must be activated to use configured Python!) +-- `${OPENPYPE_ROOT}/python start.py runtests` By default, this command will run all tests in ${OPENPYPE_ROOT}/tests. Specific location could be provided to this command as an argument, either as absolute path, or relative path to ${OPENPYPE_ROOT}. -(eg. `${OPENPYPE_ROOT}/start.py runtests ../tests/integration`) will trigger only tests in `integration` folder. +(eg. `${OPENPYPE_ROOT}/python start.py runtests ../tests/integration`) will trigger only tests in `integration` folder. See `${OPENPYPE_ROOT}/cli.py:runtests` for other arguments. diff --git a/tests/integration/README.md b/tests/integration/README.md index 81c07ec50c..0b6a1804ae 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -5,33 +5,64 @@ Contains end-to-end tests for automatic testing of OP. Should run headless publish on all hosts to check basic publish use cases automatically to limit regression issues. +How to run +---------- +- activate `{OPENPYPE_ROOT}/.venv` +- run in cmd +`{OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests {OPENPYPE_ROOT}/tests/integration` + - add `hosts/APP_NAME` after integration part to limit only on specific app (eg. `{OPENPYPE_ROOT}/tests/integration/hosts/maya`) + +OR can use built executables +`openpype_console runtests {ABS_PATH}/tests/integration` + +How to check logs/errors from app +-------------------------------- +Keep PERSIST to True in the class and check `test_openpype.logs` collection. + How to create test for publishing from host ------------------------------------------ -- Extend PublishTest +- Extend PublishTest in `tests/lib/testing_classes.py` - Use `resources\test_data.zip` skeleton file as a template for testing input data - Put workfile into `test_data.zip/input/workfile` - If you require other than base DB dumps provide them to `test_data.zip/input/dumps` -- (Check commented code in `db_handler.py` how to dump specific DB. Currently all collections will be dumped.) - Implement `last_workfile_path` - `startup_scripts` - must contain pointing host to startup script saved into `test_data.zip/input/startup` - -- Script must contain something like + -- Script must contain something like (pseudocode) ``` import openpype from avalon import api, HOST + +from openpype.api import Logger + +log = Logger().get_logger(__name__) api.install(HOST) -pyblish.util.publish() +log_lines = [] +for result in pyblish.util.publish_iter(): + for record in result["records"]: # for logging to test_openpype DB + log_lines.append("{}: {}".format( + result["plugin"].label, record.msg)) + + if result["error"]: + err_fmt = "Failed {plugin.__name__}: {error} -- {error.traceback}" + log.error(err_fmt.format(**result)) EXIT_APP (command to exit host) ``` (Install and publish methods must be triggered only AFTER host app is fully initialized!) -- Zip `test_data.zip`, named it with descriptive name, upload it to Google Drive, right click - `Get link`, copy hash id +- If you would like add any command line arguments for your host app add it to `test_data.zip/input/app_args/app_args.json` (as a json list) +- Provide any required environment variables to `test_data.zip/input/env_vars/env_vars.json` (as a json dictionary) +- Zip `test_data.zip`, named it with descriptive name, upload it to Google Drive, right click - `Get link`, copy hash id (file must be accessible to anyone with a link!) - Put this hash id and zip file name into TEST_FILES [(HASH_ID, FILE_NAME, MD5_OPTIONAL)]. If you want to check MD5 of downloaded file, provide md5 value of zipped file. - Implement any assert checks you need in extended class - Run test class manually (via Pycharm or pytest runner (TODO)) -- If you want test to compare expected files to published one, set PERSIST to True, run test manually +- If you want test to visually compare expected files to published one, set PERSIST to True, run test manually -- Locate temporary `publish` subfolder of temporary folder (found in debugging console log) -- Copy whole folder content into .zip file into `expected` subfolder -- By default tests are comparing only structure of `expected` and published format (eg. if you want to save space, replace published files with empty files, but with expected names!) - -- Zip and upload again, change PERSIST to False \ No newline at end of file + -- Zip and upload again, change PERSIST to False + +- Use `TEST_DATA_FOLDER` variable in your class to reuse existing downloaded and unzipped test data (for faster creation of tests) +- Keep `APP_VARIANT` empty if you want to trigger test on latest version of app, or provide explicit value (as '2022' for Photoshop for example) \ No newline at end of file diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000000..400c0dcc2a --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# adds command line arguments for 'runtests' as a fixtures +import pytest + + +def pytest_addoption(parser): + parser.addoption( + "--test_data_folder", action="store", default=None, + help="Provide url of a folder of unzipped test file" + ) + + parser.addoption( + "--persist", action="store", default=None, + help="True - keep test_db, test_openpype, outputted test files" + ) + + parser.addoption( + "--app_variant", action="store", default=None, + help="Keep empty to locate latest installed variant or explicit" + ) + + +@pytest.fixture(scope="module") +def test_data_folder(request): + return request.config.getoption("--test_data_folder") + + +@pytest.fixture(scope="module") +def persist(request): + return request.config.getoption("--persist") + + +@pytest.fixture(scope="module") +def app_variant(request): + return request.config.getoption("--app_variant") diff --git a/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py new file mode 100644 index 0000000000..407c4f8a3a --- /dev/null +++ b/tests/integration/hosts/aftereffects/test_publish_in_aftereffects.py @@ -0,0 +1,102 @@ +import pytest +import os +import shutil + +from tests.lib.testing_classes import PublishTest + + +class TestPublishInAfterEffects(PublishTest): + """Basic test case for publishing in AfterEffects + + Uses generic TestCase to prepare fixtures for test data, testing DBs, + env vars. + + Opens AfterEffects, run publish on prepared workile. + + Test zip file sets 3 required env vars: + - HEADLESS_PUBLISH - this triggers publish immediately app is open + - IS_TEST - this differentiate between regular webpublish + - PYBLISH_TARGETS + + Then checks content of DB (if subset, version, representations were + created. + Checks tmp folder if all expected files were published. + + """ + PERSIST = True + + TEST_FILES = [ + ("1c8261CmHwyMgS-g7S4xL5epAp0jCBmhf", + "test_aftereffects_publish.zip", + "") + ] + + APP = "aftereffects" + APP_VARIANT = "2022" + + APP_NAME = "{}/{}".format(APP, APP_VARIANT) + + TIMEOUT = 120 # publish timeout + + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "workfile", + "test_project_test_asset_TestTask_v001.aep") + dest_folder = os.path.join(download_test_data, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.aep") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points AfterEffects to userSetup file from input data""" + pass + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + + assert 2 == dbcon.count_documents({"type": "version"}), \ + "Not expected no of versions" + + assert 0 == dbcon.count_documents({"type": "version", + "name": {"$ne": 1}}), \ + "Only versions with 1 expected" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "imageMainBackgroundcopy" + }), \ + "modelMain subset must be present" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "workfileTest_task"}), \ + "workfileTesttask subset must be present" + + assert 1 == dbcon.count_documents({"type": "subset", + "name": "reviewTesttask"}), \ + "reviewTesttask subset must be present" + + assert 4 == dbcon.count_documents({"type": "representation"}), \ + "Not expected no of representations" + + assert 1 == dbcon.count_documents({"type": "representation", + "context.subset": "renderTestTaskDefault", # noqa E501 + "context.ext": "png"}), \ + "Not expected no of representations with ext 'png'" + + +if __name__ == "__main__": + test_case = TestPublishInAfterEffects() diff --git a/tests/integration/hosts/maya/lib.py b/tests/integration/hosts/maya/lib.py new file mode 100644 index 0000000000..f3a438c065 --- /dev/null +++ b/tests/integration/hosts/maya/lib.py @@ -0,0 +1,41 @@ +import os +import pytest +import shutil + +from tests.lib.testing_classes import HostFixtures + + +class MayaTestClass(HostFixtures): + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "workfile", + "test_project_test_asset_TestTask_v001.mb") + dest_folder = os.path.join(output_folder_url, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.mb") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points Maya to userSetup file from input data""" + startup_path = os.path.join(download_test_data, + "input", + "startup") + original_pythonpath = os.environ.get("PYTHONPATH") + monkeypatch_session.setenv("PYTHONPATH", + "{}{}{}".format(startup_path, + os.pathsep, + original_pythonpath)) diff --git a/tests/integration/hosts/maya/test_publish_in_maya.py b/tests/integration/hosts/maya/test_publish_in_maya.py index 1babf30029..68b0564428 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_publish_in_maya.py @@ -1,11 +1,7 @@ -import pytest -import os -import shutil - -from tests.lib.testing_classes import PublishTest +from tests.integration.hosts.maya.lib import MayaTestClass -class TestPublishInMaya(PublishTest): +class TestPublishInMaya(MayaTestClass): """Basic test case for publishing in Maya Shouldnt be running standalone only via 'runtests' pype command! (??) @@ -13,60 +9,31 @@ class TestPublishInMaya(PublishTest): Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. - Opens Maya, run publish on prepared workile. + Always pulls and uses test data from GDrive! + + Opens Maya, runs publish on prepared workile. Then checks content of DB (if subset, version, representations were created. Checks tmp folder if all expected files were published. + How to run: + (in cmd with activated {OPENPYPE_ROOT}/.venv) + {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/maya # noqa: E501 + """ - PERSIST = True + PERSIST = False TEST_FILES = [ - ("1pOwjA_VVBc6ooTZyFxtAwLS2KZHaBlkY", "test_maya_publish.zip", "") + ("1BTSIIULJTuDc8VvXseuiJV_fL6-Bu7FP", "test_maya_publish.zip", "") ] APP = "maya" - APP_VARIANT = "2019" - - APP_NAME = "{}/{}".format(APP, APP_VARIANT) + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" TIMEOUT = 120 # publish timeout - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): - """Get last_workfile_path from source data. - - Maya expects workfile in proper folder, so copy is done first. - """ - src_path = os.path.join(download_test_data, - "input", - "workfile", - "test_project_test_asset_TestTask_v001.mb") - dest_folder = os.path.join(download_test_data, - self.PROJECT, - self.ASSET, - "work", - self.TASK) - os.makedirs(dest_folder) - dest_path = os.path.join(dest_folder, - "test_project_test_asset_TestTask_v001.mb") - shutil.copy(src_path, dest_path) - - yield dest_path - - @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - """Points Maya to userSetup file from input data""" - startup_path = os.path.join(download_test_data, - "input", - "startup") - original_pythonpath = os.environ.get("PYTHONPATH") - monkeypatch_session.setenv("PYTHONPATH", - "{}{}{}".format(startup_path, - os.pathsep, - original_pythonpath)) - def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") diff --git a/tests/integration/hosts/nuke/lib.py b/tests/integration/hosts/nuke/lib.py new file mode 100644 index 0000000000..d3c3d7ba81 --- /dev/null +++ b/tests/integration/hosts/nuke/lib.py @@ -0,0 +1,44 @@ +import os +import pytest +import shutil + +from tests.lib.testing_classes import HostFixtures + + +class NukeTestClass(HostFixtures): + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Get last_workfile_path from source data. + + """ + source_file_name = "test_project_test_asset_CompositingInNuke_v001.nk" + src_path = os.path.join(download_test_data, + "input", + "workfile", + source_file_name) + dest_folder = os.path.join(output_folder_url, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) + + dest_path = os.path.join(dest_folder, + source_file_name) + + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points Nuke to userSetup file from input data""" + startup_path = os.path.join(download_test_data, + "input", + "startup") + original_nuke_path = os.environ.get("NUKE_PATH", "") + monkeypatch_session.setenv("NUKE_PATH", + "{}{}{}".format(startup_path, + os.pathsep, + original_nuke_path)) \ No newline at end of file diff --git a/tests/integration/hosts/nuke/test_publish_in_nuke.py b/tests/integration/hosts/nuke/test_publish_in_nuke.py new file mode 100644 index 0000000000..884160e0b5 --- /dev/null +++ b/tests/integration/hosts/nuke/test_publish_in_nuke.py @@ -0,0 +1,74 @@ +import logging + +from tests.lib.assert_classes import DBAssert +from tests.integration.hosts.nuke.lib import NukeTestClass + +log = logging.getLogger("test_publish_in_nuke") + + +class TestPublishInNuke(NukeTestClass): + """Basic test case for publishing in Nuke + + Uses generic TestCase to prepare fixtures for test data, testing DBs, + env vars. + + Opens Nuke, run publish on prepared workile. + + Then checks content of DB (if subset, version, representations were + created. + Checks tmp folder if all expected files were published. + + How to run: + (in cmd with activated {OPENPYPE_ROOT}/.venv) + {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/nuke # noqa: E501 + + To check log/errors from launched app's publish process keep PERSIST + to True and check `test_openpype.logs` collection. + """ + # https://drive.google.com/file/d/1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI/view?usp=sharing # noqa: E501 + TEST_FILES = [ + ("1SUurHj2aiQ21ZIMJfGVBI2KjR8kIjBGI", "test_Nuke_publish.zip", "") + ] + + APP = "nuke" + + TIMEOUT = 120 # publish timeout + + # could be overwritten by command line arguments + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + PERSIST = True # True - keep test_db, test_openpype, outputted test files + TEST_DATA_FOLDER = None + + def test_db_asserts(self, dbcon, publish_finished): + """Host and input data dependent expected results in DB.""" + print("test_db_asserts") + failures = [] + + failures.append(DBAssert.count_of_types(dbcon, "version", 2)) + + failures.append( + DBAssert.count_of_types(dbcon, "version", 0, name={"$ne": 1})) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="renderCompositingInNukeMain")) + + failures.append( + DBAssert.count_of_types(dbcon, "subset", 1, + name="workfileTest_task")) + + failures.append( + DBAssert.count_of_types(dbcon, "representation", 4)) + + additional_args = {"context.subset": "renderCompositingInNukeMain", + "context.ext": "exr"} + failures.append( + DBAssert.count_of_types(dbcon, "representation", 1, + additional_args=additional_args)) + + assert not any(failures) + + +if __name__ == "__main__": + test_case = TestPublishInNuke() diff --git a/tests/integration/hosts/photoshop/lib.py b/tests/integration/hosts/photoshop/lib.py new file mode 100644 index 0000000000..16ef2d3ae6 --- /dev/null +++ b/tests/integration/hosts/photoshop/lib.py @@ -0,0 +1,34 @@ +import os +import pytest +import shutil + +from tests.lib.testing_classes import HostFixtures + + +class PhotoshopTestClass(HostFixtures): + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Get last_workfile_path from source data. + + Maya expects workfile in proper folder, so copy is done first. + """ + src_path = os.path.join(download_test_data, + "input", + "workfile", + "test_project_test_asset_TestTask_v001.psd") + dest_folder = os.path.join(output_folder_url, + self.PROJECT, + self.ASSET, + "work", + self.TASK) + os.makedirs(dest_folder) + dest_path = os.path.join(dest_folder, + "test_project_test_asset_TestTask_v001.psd") + shutil.copy(src_path, dest_path) + + yield dest_path + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """Points Maya to userSetup file from input data""" + pass diff --git a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py index 396468a966..32053cd9d4 100644 --- a/tests/integration/hosts/photoshop/test_publish_in_photoshop.py +++ b/tests/integration/hosts/photoshop/test_publish_in_photoshop.py @@ -1,67 +1,54 @@ -import pytest -import os -import shutil - -from tests.lib.testing_classes import PublishTest +from tests.integration.hosts.photoshop.lib import PhotoshopTestClass -class TestPublishInPhotoshop(PublishTest): +class TestPublishInPhotoshop(PhotoshopTestClass): """Basic test case for publishing in Photoshop Uses generic TestCase to prepare fixtures for test data, testing DBs, env vars. - Opens Maya, run publish on prepared workile. + Opens Photoshop, run publish on prepared workile. + + Test zip file sets 3 required env vars: + - HEADLESS_PUBLISH - this triggers publish immediately app is open + - IS_TEST - this differentiate between regular webpublish + - PYBLISH_TARGETS + + Always pulls and uses test data from GDrive! + + Test zip file sets 3 required env vars: + - HEADLESS_PUBLISH - this triggers publish immediately app is open + - IS_TEST - this differentiate between regular webpublish + - PYBLISH_TARGETS Then checks content of DB (if subset, version, representations were created. Checks tmp folder if all expected files were published. + How to run: + (in cmd with activated {OPENPYPE_ROOT}/.venv) + {OPENPYPE_ROOT}/.venv/Scripts/python.exe {OPENPYPE_ROOT}/start.py runtests ../tests/integration/hosts/photoshop # noqa: E501 + """ - PERSIST = True + PERSIST = False TEST_FILES = [ - ("1Bciy2pCwMKl1UIpxuPnlX_LHMo_Xkq0K", "test_photoshop_publish.zip", "") + ("1zD2v5cBgkyOm_xIgKz3WKn8aFB_j8qC-", "test_photoshop_publish.zip", "") ] APP = "photoshop" - APP_VARIANT = "2020" + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" APP_NAME = "{}/{}".format(APP, APP_VARIANT) TIMEOUT = 120 # publish timeout - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): - """Get last_workfile_path from source data. - - Maya expects workfile in proper folder, so copy is done first. - """ - src_path = os.path.join(download_test_data, - "input", - "workfile", - "test_project_test_asset_TestTask_v001.psd") - dest_folder = os.path.join(download_test_data, - self.PROJECT, - self.ASSET, - "work", - self.TASK) - os.makedirs(dest_folder) - dest_path = os.path.join(dest_folder, - "test_project_test_asset_TestTask_v001.psd") - shutil.copy(src_path, dest_path) - - yield dest_path - - @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - """Points Maya to userSetup file from input data""" - os.environ["IS_HEADLESS"] = "true" def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" print("test_db_asserts") - assert 5 == dbcon.count_documents({"type": "version"}), \ + assert 3 == dbcon.count_documents({"type": "version"}), \ "Not expected no of versions" assert 0 == dbcon.count_documents({"type": "version", @@ -69,25 +56,21 @@ class TestPublishInPhotoshop(PublishTest): "Only versions with 1 expected" assert 1 == dbcon.count_documents({"type": "subset", - "name": "modelMain"}), \ + "name": "imageMainBackgroundcopy"} + ), \ "modelMain subset must be present" assert 1 == dbcon.count_documents({"type": "subset", - "name": "workfileTest_task"}), \ + "name": "workfileTesttask"}), \ "workfileTest_task subset must be present" - assert 11 == dbcon.count_documents({"type": "representation"}), \ + assert 6 == dbcon.count_documents({"type": "representation"}), \ "Not expected no of representations" - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "abc"}), \ - "Not expected no of representations with ext 'abc'" - - assert 2 == dbcon.count_documents({"type": "representation", - "context.subset": "modelMain", - "context.ext": "ma"}), \ - "Not expected no of representations with ext 'abc'" + assert 1 == dbcon.count_documents({"type": "representation", + "context.subset": "imageMainBackgroundcopy", # noqa: E501 + "context.ext": "png"}), \ + "Not expected no of representations with ext 'png'" if __name__ == "__main__": diff --git a/tests/lib/assert_classes.py b/tests/lib/assert_classes.py new file mode 100644 index 0000000000..7298853b67 --- /dev/null +++ b/tests/lib/assert_classes.py @@ -0,0 +1,45 @@ +"""Classed and methods for comparing expected and published items in DBs""" + +class DBAssert: + + @classmethod + def count_of_types(cls, dbcon, queried_type, expected, **kwargs): + """Queries 'dbcon' and counts documents of type 'queried_type' + + Args: + dbcon (AvalonMongoDB) + queried_type (str): type of document ("asset", "version"...) + expected (int): number of documents found + any number of additional keyword arguments + + special handling of argument additional_args (dict) + with additional args like + {"context.subset": "XXX"} + """ + args = {"type": queried_type} + for key, val in kwargs.items(): + if key == "additional_args": + args.update(val) + else: + args[key] = val + + msg = None + no_of_docs = dbcon.count_documents(args) + if expected != no_of_docs: + msg = "Not expected no of versions. "\ + "Expected {}, found {}".format(expected, no_of_docs) + + args.pop("type") + detail_str = " " + if args: + detail_str = " with {}".format(args) + + status = "successful" + if msg: + status = "failed" + + print("Comparing count of {}{} {}".format(queried_type, + detail_str, + status)) + + return msg diff --git a/tests/lib/db_handler.py b/tests/lib/db_handler.py index 9be70895da..b181055012 100644 --- a/tests/lib/db_handler.py +++ b/tests/lib/db_handler.py @@ -112,9 +112,17 @@ class DBHandler: source 'db_name' """ db_name_out = db_name_out or db_name - if self._db_exists(db_name) and not overwrite: - raise RuntimeError("DB {} already exists".format(db_name_out) + - "Run with overwrite=True") + if self._db_exists(db_name_out): + if not overwrite: + raise RuntimeError("DB {} already exists".format(db_name_out) + + "Run with overwrite=True") + else: + if collection: + coll = self.client[db_name_out].get(collection) + if coll: + coll.drop() + else: + self.teardown(db_name_out) dir_path = os.path.join(dump_dir, db_name) if not os.path.exists(dir_path): @@ -136,7 +144,8 @@ class DBHandler: print("Dropping {} database".format(db_name)) self.client.drop_database(db_name) - def backup_to_dump(self, db_name, dump_dir, overwrite=False): + def backup_to_dump(self, db_name, dump_dir, overwrite=False, + collection=None): """ Helper method for running mongodump for specific 'db_name' """ @@ -148,7 +157,8 @@ class DBHandler: raise RuntimeError("Backup already exists, " "run with overwrite=True") - query = self._dump_query(self.uri, dump_dir, db_name=db_name) + query = self._dump_query(self.uri, dump_dir, + db_name=db_name, collection=collection) print("Mongodump query:: {}".format(query)) subprocess.run(query) @@ -163,7 +173,7 @@ class DBHandler: if collection: if not db_name: raise ValueError("db_name must be present") - coll_part = "--nsInclude={}.{}".format(db_name, collection) + coll_part = "--collection={}".format(collection) query = "\"{}\" --uri=\"{}\" --out={} {} {}".format( "mongodump", uri, output_path, db_part, coll_part ) @@ -187,7 +197,8 @@ class DBHandler: drop_part = "--drop" if db_name_out: - db_part += " --nsTo={}.*".format(db_name_out) + collection_str = collection or '*' + db_part += " --nsTo={}.{}".format(db_name_out, collection_str) query = "\"{}\" --uri=\"{}\" --dir=\"{}\" {} {} {}".format( "mongorestore", uri, dump_dir, db_part, coll_part, drop_part @@ -217,15 +228,16 @@ class DBHandler: return query +# Examples # handler = DBHandler(uri="mongodb://localhost:27017") -# -# backup_dir = "c:\\projects\\dumps" # # -# handler.backup_to_dump("openpype", backup_dir, True) -# # handler.setup_from_dump("test_db", backup_dir, True) -# # handler.setup_from_sql_file("test_db", "c:\\projects\\sql\\item.sql", -# # collection="test_project", -# # drop=False, mode="upsert") -# handler.setup_from_sql("test_db", "c:\\projects\\sql", +# backup_dir = "c:\\projects\\test_nuke_publish\\input\\dumps" +# # # +# handler.backup_to_dump("avalon", backup_dir, True, collection="test_project") +# handler.setup_from_dump("test_db", backup_dir, True, db_name_out="avalon", collection="test_project") +# handler.setup_from_sql_file("test_db", "c:\\projects\\sql\\item.sql", # collection="test_project", # drop=False, mode="upsert") +# handler.setup_from_sql("test_db", "c:\\projects\\sql", +# collection="test_project", +# drop=False, mode="upsert") diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 59d4abb3aa..fa467acf9c 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -7,10 +7,13 @@ import pytest import tempfile import shutil import glob +import platform from tests.lib.db_handler import DBHandler from tests.lib.file_handler import RemoteFileHandler +from openpype.lib.remote_publish import find_variant_key + class BaseTest: """Empty base test class""" @@ -45,6 +48,8 @@ class ModuleUnitTest(BaseTest): ASSET = "test_asset" TASK = "test_task" + TEST_DATA_FOLDER = None + @pytest.fixture(scope='session') def monkeypatch_session(self): """Monkeypatch couldn't be used with module or session fixtures.""" @@ -54,25 +59,31 @@ class ModuleUnitTest(BaseTest): m.undo() @pytest.fixture(scope="module") - def download_test_data(self): - tmpdir = tempfile.mkdtemp() - for test_file in self.TEST_FILES: - file_id, file_name, md5 = test_file + def download_test_data(self, test_data_folder, persist=False): + test_data_folder = test_data_folder or self.TEST_DATA_FOLDER + if test_data_folder: + print("Using existing folder {}".format(test_data_folder)) + yield test_data_folder + else: + tmpdir = tempfile.mkdtemp() + for test_file in self.TEST_FILES: + file_id, file_name, md5 = test_file - f_name, ext = os.path.splitext(file_name) + f_name, ext = os.path.splitext(file_name) - RemoteFileHandler.download_file_from_google_drive(file_id, - str(tmpdir), - file_name) + RemoteFileHandler.download_file_from_google_drive(file_id, + str(tmpdir), + file_name) - if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: - RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) - print("Temporary folder created:: {}".format(tmpdir)) - yield tmpdir + if ext.lstrip('.') in RemoteFileHandler.IMPLEMENTED_ZIP_FORMATS: # noqa: E501 + RemoteFileHandler.unzip(os.path.join(tmpdir, file_name)) + print("Temporary folder created:: {}".format(tmpdir)) + yield tmpdir - if not self.PERSIST: - print("Removing {}".format(tmpdir)) - shutil.rmtree(tmpdir) + persist = persist or self.PERSIST + if not persist: + print("Removing {}".format(tmpdir)) + shutil.rmtree(tmpdir) @pytest.fixture(scope="module") def env_var(self, monkeypatch_session, download_test_data): @@ -97,13 +108,24 @@ class ModuleUnitTest(BaseTest): value = value.format(**all_vars) print("Setting {}:{}".format(key, value)) monkeypatch_session.setenv(key, str(value)) - import openpype + #reset connection to openpype DB with new env var + import openpype.settings.lib as sett_lib + sett_lib._SETTINGS_HANDLER = None + sett_lib._LOCAL_SETTINGS_HANDLER = None + sett_lib.create_settings_handler() + sett_lib.create_local_settings_handler() + + import openpype openpype_root = os.path.dirname(os.path.dirname(openpype.__file__)) + # ?? why 2 of those monkeypatch_session.setenv("OPENPYPE_ROOT", openpype_root) monkeypatch_session.setenv("OPENPYPE_REPOS_ROOT", openpype_root) + # for remapping purposes (currently in Nuke) + monkeypatch_session.setenv("TEST_SOURCE_FOLDER", download_test_data) + @pytest.fixture(scope="module") def db_setup(self, download_test_data, env_var, monkeypatch_session): """Restore prepared MongoDB dumps into selected DB.""" @@ -111,10 +133,12 @@ class ModuleUnitTest(BaseTest): uri = os.environ.get("OPENPYPE_MONGO") db_handler = DBHandler(uri) - db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir, True, + db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir, + overwrite=True, db_name_out=self.TEST_DB_NAME) - db_handler.setup_from_dump("openpype", backup_dir, True, + db_handler.setup_from_dump("openpype", backup_dir, + overwrite=True, db_name_out=self.TEST_OPENPYPE_NAME) yield db_handler @@ -167,31 +191,76 @@ class PublishTest(ModuleUnitTest): """ APP = "" - APP_VARIANT = "" - - APP_NAME = "{}/{}".format(APP, APP_VARIANT) TIMEOUT = 120 # publish timeout - @pytest.fixture(scope="module") - def last_workfile_path(self, download_test_data): - raise NotImplementedError + # could be overwritten by command line arguments + # command line value takes precedence + + # keep empty to locate latest installed variant or explicit + APP_VARIANT = "" + PERSIST = True # True - keep test_db, test_openpype, outputted test files + TEST_DATA_FOLDER = None # use specific folder of unzipped test file @pytest.fixture(scope="module") - def startup_scripts(self, monkeypatch_session, download_test_data): - raise NotImplementedError + def app_name(self, app_variant): + """Returns calculated value for ApplicationManager. Eg.(nuke/12-2)""" + from openpype.lib import ApplicationManager + app_variant = app_variant or self.APP_VARIANT + + application_manager = ApplicationManager() + if not app_variant: + app_variant = find_variant_key(application_manager, self.APP) + + yield "{}/{}".format(self.APP, app_variant) + + @pytest.fixture(scope="module") + def output_folder_url(self, download_test_data): + """Returns location of published data, cleans it first if exists.""" + path = os.path.join(download_test_data, "output") + if os.path.exists(path): + print("Purging {}".format(path)) + shutil.rmtree(path) + yield path + + @pytest.fixture(scope="module") + def app_args(self, download_test_data): + """Returns additional application arguments from a test file. + + Test zip file should contain file at: + FOLDER_DIR/input/app_args/app_args.json + containing a list of command line arguments (like '-x' etc.) + """ + app_args = [] + args_url = os.path.join(download_test_data, "input", + "app_args", "app_args.json") + if not os.path.exists(args_url): + print("App argument file {} doesn't exist".format(args_url)) + else: + try: + with open(args_url) as json_file: + app_args = json.load(json_file) + + if not isinstance(app_args, list): + raise ValueError + except ValueError: + print("{} doesn't contain valid JSON".format(args_url)) + six.reraise(*sys.exc_info()) + + yield app_args @pytest.fixture(scope="module") def launched_app(self, dbcon, download_test_data, last_workfile_path, - startup_scripts): + startup_scripts, app_args, app_name, output_folder_url): """Launch host app""" # set publishing folders - root_key = "config.roots.work.{}".format("windows") # TEMP + platform_str = platform.system().lower() + root_key = "config.roots.work.{}".format(platform_str) dbcon.update_one( {"type": "project"}, {"$set": { - root_key: download_test_data + root_key: output_folder_url }} ) @@ -217,8 +286,11 @@ class PublishTest(ModuleUnitTest): "asset_name": self.ASSET, "task_name": self.TASK } + if app_args: + data["app_args"] = app_args - yield application_manager.launch(self.APP_NAME, **data) + app_process = application_manager.launch(app_name, **data) + yield app_process @pytest.fixture(scope="module") def publish_finished(self, dbcon, launched_app, download_test_data): @@ -236,23 +308,26 @@ class PublishTest(ModuleUnitTest): yield True def test_folder_structure_same(self, dbcon, publish_finished, - download_test_data): + download_test_data, output_folder_url): """Check if expected and published subfolders contain same files. Compares only presence, not size nor content! """ published_dir_base = download_test_data - published_dir = os.path.join(published_dir_base, + published_dir = os.path.join(output_folder_url, self.PROJECT, + self.ASSET, self.TASK, "**") expected_dir_base = os.path.join(published_dir_base, "expected") expected_dir = os.path.join(expected_dir_base, self.PROJECT, + self.ASSET, self.TASK, "**") - + print("Comparing published:'{}' : expected:'{}'".format(published_dir, + expected_dir)) published = set(f.replace(published_dir_base, '') for f in glob.glob(published_dir, recursive=True) if f != published_dir_base and os.path.exists(f)) @@ -262,3 +337,16 @@ class PublishTest(ModuleUnitTest): not_matched = expected.difference(published) assert not not_matched, "Missing {} files".format(not_matched) + + +class HostFixtures(PublishTest): + """Host specific fixtures. Should be implemented once per host.""" + @pytest.fixture(scope="module") + def last_workfile_path(self, download_test_data, output_folder_url): + """Returns url of workfile""" + raise NotImplementedError + + @pytest.fixture(scope="module") + def startup_scripts(self, monkeypatch_session, download_test_data): + """"Adds init scripts (like userSetup) to expected location""" + raise NotImplementedError \ No newline at end of file diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md index 30784686e2..1f742c31ed 100644 --- a/website/docs/admin_settings_project_anatomy.md +++ b/website/docs/admin_settings_project_anatomy.md @@ -60,6 +60,7 @@ We have a few required anatomy templates for OpenPype to work properly, however | `task[name]` | Name of task | | `task[type]` | Type of task | | `task[short]` | Shortname of task | +| `parent` | Name of hierarchical parent | | `version` | Version number | | `subset` | Subset name | | `family` | Main family name |