From 8b0036cf53426068ac855f411e18b3e6ab418c30 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 9 Jun 2021 18:19:40 +0200 Subject: [PATCH 001/333] fix unreal launch on linux --- openpype/hosts/unreal/api/lib.py | 212 +++++++++++------- .../unreal/hooks/pre_workfile_preparation.py | 10 +- 2 files changed, 139 insertions(+), 83 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 7e706a2789..3efc0c63be 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -1,38 +1,50 @@ +# -*- coding: utf-8 -*- +"""Unreal launching and project tools.""" import sys import os import platform import json from distutils import dir_util import subprocess +import re +from collections import OrderedDict from openpype.api import get_project_settings -def get_engine_versions(): - """ +def get_engine_versions(env=None): + """Detect Unreal Engine versions. + This will try to detect location and versions of installed Unreal Engine. Location can be overridden by `UNREAL_ENGINE_LOCATION` environment variable. - Returns: + Args: + env (dict, optional): Environment to use. - dict: dictionary with version as a key and dir as value. + Returns: + OrderedDict: dictionary with version as a key and dir as value. + so the highest version is first. Example: - - >>> get_engine_version() + >>> get_engine_versions() { "4.23": "C:/Epic Games/UE_4.23", "4.24": "C:/Epic Games/UE_4.24" } - """ - try: - engine_locations = {} - root, dirs, files = next(os.walk(os.environ["UNREAL_ENGINE_LOCATION"])) - for dir in dirs: - if dir.startswith("UE_"): - ver = dir.split("_")[1] - engine_locations[ver] = os.path.join(root, dir) + """ + env = env or os.environ + engine_locations = {} + try: + root, dirs, _ = next(os.walk(env["UNREAL_ENGINE_LOCATION"])) + + for directory in dirs: + if directory.startswith("UE"): + try: + ver = re.split(r"[-_]", directory)[1] + except IndexError: + continue + engine_locations[ver] = os.path.join(root, directory) except KeyError: # environment variable not set pass @@ -40,32 +52,37 @@ def get_engine_versions(): # specified directory doesn't exists pass - # if we've got something, terminate autodetection process + # if we've got something, terminate auto-detection process if engine_locations: - return engine_locations + return OrderedDict(sorted(engine_locations.items())) # else kick in platform specific detection if platform.system().lower() == "windows": - return _win_get_engine_versions() + return OrderedDict(sorted(_win_get_engine_versions().items())) elif platform.system().lower() == "linux": # on linux, there is no installation and getting Unreal Engine involves # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. pass elif platform.system().lower() == "darwin": - return _darwin_get_engine_version() + return OrderedDict(sorted(_darwin_get_engine_version(env).items())) - return {} + return OrderedDict() def _win_get_engine_versions(): - """ + """Get Unreal Engine versions on Windows. + If engines are installed via Epic Games Launcher then there is: `%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat` This file is JSON file listing installed stuff, Unreal engines are marked with `"AppName" = "UE_X.XX"`` like `UE_4.24` + + Returns: + dict: version as a key and path as a value. + """ install_json_path = os.path.join( - os.environ.get("PROGRAMDATA"), + os.getenv("PROGRAMDATA"), "Epic", "UnrealEngineLauncher", "LauncherInstalled.dat", @@ -75,11 +92,19 @@ def _win_get_engine_versions(): def _darwin_get_engine_version() -> dict: - """ + """Get Unreal Engine versions on MacOS. + It works the same as on Windows, just JSON file location is different. + + Returns: + dict: version as a key and path as a value. + + See Aslo: + :func:`_win_get_engine_versions`. + """ install_json_path = os.path.join( - os.environ.get("HOME"), + os.getenv("HOME"), "Library", "Application Support", "Epic", @@ -91,25 +116,26 @@ def _darwin_get_engine_version() -> dict: def _parse_launcher_locations(install_json_path: str) -> dict: - """ - This will parse locations from json file. + """This will parse locations from json file. + + Args: + install_json_path (str): Path to `LauncherInstalled.dat`. + + Returns: + dict: with unreal engine versions as keys and + paths to those engine installations as value. - :param install_json_path: path to `LauncherInstalled.dat` - :type install_json_path: str - :returns: returns dict with unreal engine versions as keys and - paths to those engine installations as value. - :rtype: dict """ engine_locations = {} if os.path.isfile(install_json_path): with open(install_json_path, "r") as ilf: try: install_data = json.load(ilf) - except json.JSONDecodeError: + except json.JSONDecodeError as e: raise Exception( "Invalid `LauncherInstalled.dat file. `" "Cannot determine Unreal Engine location." - ) + ) from e for installation in install_data.get("InstallationList", []): if installation.get("AppName").startswith("UE_"): @@ -123,36 +149,43 @@ def create_unreal_project(project_name: str, ue_version: str, pr_dir: str, engine_path: str, - dev_mode: bool = False) -> None: - """ - This will create `.uproject` file at specified location. As there is no - way I know to create project via command line, this is easiest option. - Unreal project file is basically JSON file. If we find + dev_mode: bool = False, + env: dict = None) -> None: + """This will create `.uproject` file at specified location. + + As there is no way I know to create project via command line, this is + easiest option. Unreal project file is basically JSON file. If we find `AVALON_UNREAL_PLUGIN` environment variable we assume this is location of Avalon Integration Plugin and we copy its content to project folder and enable this plugin. - :param project_name: project name - :type project_name: str - :param ue_version: unreal engine version (like 4.23) - :type ue_version: str - :param pr_dir: path to directory where project will be created - :type pr_dir: str - :param engine_path: Path to Unreal Engine installation - :type engine_path: str - :param dev_mode: Flag to trigger C++ style Unreal project needing - Visual Studio and other tools to compile plugins from - sources. This will trigger automatically if `Binaries` - directory is not found in plugin folders as this indicates - this is only source distribution of the plugin. Dev mode - is also set by preset file `unreal/project_setup.json` in - **OPENPYPE_CONFIG**. - :type dev_mode: bool - :returns: None - """ - preset = get_project_settings(project_name)["unreal"]["project_setup"] + Args: + project_name (str): Name of the project. + ue_version (str): Unreal engine version (like 4.23). + pr_dir (str): Path to directory where project will be created. + engine_path (str): Path to Unreal Engine installation. + dev_mode (bool, optional): Flag to trigger C++ style Unreal project + needing Visual Studio and other tools to compile plugins from + sources. This will trigger automatically if `Binaries` + directory is not found in plugin folders as this indicates + this is only source distribution of the plugin. Dev mode + is also set by preset file `unreal/project_setup.json` in + **OPENPYPE_CONFIG**. + env (dict, optional): Environment to use. If not set, `os.environ`. - if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): + Throws: + NotImplemented: For unsupported platforms. + + Returns: + None + + """ + env = env or os.environ + preset = get_project_settings(project_name)["unreal"]["project_setup"] + plugins_path = None + uep_path = None + + if os.path.isdir(env.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project plugins_path = os.path.join(pr_dir, "Plugins") avalon_plugin_path = os.path.join(plugins_path, "Avalon") @@ -180,17 +213,17 @@ def create_unreal_project(project_name: str, } if preset["install_unreal_python_engine"]: - # If `OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there - # to support offline installation. + # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to + # support offline installation. # Otherwise clone UnrealEnginePython to Plugins directory # https://github.com/20tab/UnrealEnginePython.git uep_path = os.path.join(plugins_path, "UnrealEnginePython") - if os.environ.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + if env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): os.makedirs(uep_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree( - os.environ.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), uep_path) else: # WARNING: this will trigger dev_mode, because we need to compile @@ -246,25 +279,40 @@ def create_unreal_project(project_name: str, with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) - # UE < 4.26 have Python2 by default, so we need PySide - # but we will not need it in 4.26 and up - if int(ue_version.split(".")[1]) < 26: - # ensure we have PySide installed in engine - # TODO: make it work for other platforms 🍎 🐧 + # ensure we have PySide installed in engine + # this won't work probably as pyside is no longer on pypi + # DEPRECATED: support for python 2 in UE4 is dropped. + python_path = None + if int(ue_version.split(".")[0]) == 4 and \ + int(ue_version.split(".")[1]) < 25: if platform.system().lower() == "windows": python_path = os.path.join(engine_path, "Engine", "Binaries", "ThirdParty", "Python", "Win64", "python.exe") + if platform.system().lower() == "linux": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python", "Linux", + "bin", "python") + + if platform.system().lower() == "darwin": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python", "Mac", + "bin", "python") + + if python_path: subprocess.run([python_path, "-m", "pip", "install", "pyside"]) + else: + raise NotImplemented("Unsupported platform") if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) def _prepare_cpp_project(project_file: str, engine_path: str) -> None: - """ + """Prepare CPP Unreal Project. + This function will add source files needed for project to be rebuild along with the avalon integration plugin. @@ -273,12 +321,12 @@ def _prepare_cpp_project(project_file: str, engine_path: str) -> None: by some generator. This needs more research as manually writing those files is rather hackish. :skull_and_crossbones: - :param project_file: path to .uproject file - :type project_file: str - :param engine_path: path to unreal engine associated with project - :type engine_path: str - """ + Args: + project_file (str): Path to .uproject file. + engine_path (str): Path to unreal engine associated with project. + + """ project_name = os.path.splitext(os.path.basename(project_file))[0] project_dir = os.path.dirname(project_file) targets_dir = os.path.join(project_dir, "Source") @@ -388,19 +436,23 @@ class {1}_API A{0}GameModeBase : public AGameModeBase sources_dir, f"{project_name}GameModeBase.h"), mode="w") as f: f.write(game_mode_h) + u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" + "UnrealBuildTool.exe") + u_header_tool = None + if platform.system().lower() == "windows": - u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" - "UnrealBuildTool.exe") u_header_tool = (f"{engine_path}/Engine/Binaries/Win64/" f"UnrealHeaderTool.exe") elif platform.system().lower() == "linux": - # WARNING: there is no UnrealBuildTool on linux? - u_build_tool = "" - u_header_tool = "" + u_header_tool = (f"{engine_path}/Engine/Binaries/Linux/" + f"UnrealHeaderTool") elif platform.system().lower() == "darwin": - # WARNING: there is no UnrealBuildTool on Mac? - u_build_tool = "" - u_header_tool = "" + # we need to test this out + u_header_tool = (f"{engine_path}/Engine/Binaries/Mac/" + f"UnrealHeaderTool") + + if not u_header_tool: + raise NotImplemented("Unsupported platform") u_build_tool = u_build_tool.replace("\\", "/") u_header_tool = u_header_tool.replace("\\", "/") diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index f084cccfc3..7c4b6c3088 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Hook to launch Unreal and prepare projects.""" import os from openpype.lib import ( @@ -8,19 +10,21 @@ from openpype.hosts.unreal.api import lib as unreal_lib class UnrealPrelaunchHook(PreLaunchHook): - """ + """Hook to handle launching Unreal. + This hook will check if current workfile path has Unreal project inside. IF not, it initialize it and finally it pass path to the project by environment variable to Unreal launcher shell script. - """ + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.signature = "( {} )".format(self.__class__.__name__) def execute(self): + """Hook entry method.""" asset_name = self.data["asset_name"] task_name = self.data["task_name"] workdir = self.launch_context.env["AVALON_WORKDIR"] @@ -52,7 +56,7 @@ class UnrealPrelaunchHook(PreLaunchHook): f"[ {engine_version} ]" )) - detected = unreal_lib.get_engine_versions() + detected = unreal_lib.get_engine_versions(self.launch_context.env) detected_str = ', '.join(detected.keys()) or 'none' self.log.info(( f"{self.signature} detected UE4 versions: " From 969c32abd60fe66bebe04a5664aae1b79fd80187 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 9 Jun 2021 18:24:38 +0200 Subject: [PATCH 002/333] fix exceptions --- openpype/hosts/unreal/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 3efc0c63be..269ba561c7 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -174,7 +174,7 @@ def create_unreal_project(project_name: str, env (dict, optional): Environment to use. If not set, `os.environ`. Throws: - NotImplemented: For unsupported platforms. + NotImplementedError: For unsupported platforms. Returns: None @@ -304,7 +304,7 @@ def create_unreal_project(project_name: str, subprocess.run([python_path, "-m", "pip", "install", "pyside"]) else: - raise NotImplemented("Unsupported platform") + raise NotImplementedError("Unsupported platform") if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) @@ -452,7 +452,7 @@ class {1}_API A{0}GameModeBase : public AGameModeBase f"UnrealHeaderTool") if not u_header_tool: - raise NotImplemented("Unsupported platform") + raise NotImplementedError("Unsupported platform") u_build_tool = u_build_tool.replace("\\", "/") u_header_tool = u_header_tool.replace("\\", "/") From 00336a9d2d6fc0bd9a16a0b9dfb44a0bfe4af641 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 9 Jun 2021 18:27:02 +0200 Subject: [PATCH 003/333] refactor ifs --- openpype/hosts/unreal/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 269ba561c7..e069ac5256 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -59,12 +59,12 @@ def get_engine_versions(env=None): # else kick in platform specific detection if platform.system().lower() == "windows": return OrderedDict(sorted(_win_get_engine_versions().items())) - elif platform.system().lower() == "linux": + if platform.system().lower() == "linux": # on linux, there is no installation and getting Unreal Engine involves # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. pass - elif platform.system().lower() == "darwin": - return OrderedDict(sorted(_darwin_get_engine_version(env).items())) + if platform.system().lower() == "darwin": + return OrderedDict(sorted(_darwin_get_engine_version().items())) return OrderedDict() From f00e6c1195c9d5a7c5c75e9ae2c5df737ae29d13 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 15:43:21 +0200 Subject: [PATCH 004/333] set stdout and stderr of application process to devnull if sys.stdout is not set --- openpype/lib/applications.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index d82b7cd847..ff5ef92d82 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1,4 +1,5 @@ import os +import sys import re import copy import json @@ -675,6 +676,10 @@ class ApplicationLaunchContext: ) self.kwargs["creationflags"] = flags + if not sys.stdout: + self.kwargs["stdout"] = subprocess.DEVNULL + self.kwargs["stderr"] = subprocess.DEVNULL + self.prelaunch_hooks = None self.postlaunch_hooks = None From 5fa4e091e3a317e313025b922ce1ab720b437f42 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 15:43:44 +0200 Subject: [PATCH 005/333] modify stderr in non python prelaunch hook to use DEVNULL instead of STDOUT --- openpype/hooks/pre_non_python_host_launch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index 393a878f76..b91be137ab 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -49,5 +49,7 @@ class NonPythonHostHook(PreLaunchHook): if remainders: self.launch_context.launch_args.extend(remainders) + # This must be set otherwise it wouldn't be possible to catch output + # when build PpenPype is used. self.launch_context.kwargs["stdout"] = subprocess.DEVNULL - self.launch_context.kwargs["stderr"] = subprocess.STDOUT + self.launch_context.kwargs["stderr"] = subprocess.DEVNULL From 05d29dc844cc8940145b889842bb0e120c014766 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 16:06:51 +0200 Subject: [PATCH 006/333] fix typo --- openpype/hooks/pre_non_python_host_launch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_non_python_host_launch.py b/openpype/hooks/pre_non_python_host_launch.py index b91be137ab..0447f4a06f 100644 --- a/openpype/hooks/pre_non_python_host_launch.py +++ b/openpype/hooks/pre_non_python_host_launch.py @@ -50,6 +50,6 @@ class NonPythonHostHook(PreLaunchHook): self.launch_context.launch_args.extend(remainders) # This must be set otherwise it wouldn't be possible to catch output - # when build PpenPype is used. + # when build OpenPype is used. self.launch_context.kwargs["stdout"] = subprocess.DEVNULL self.launch_context.kwargs["stderr"] = subprocess.DEVNULL From 9aa83282821209248de79ad8a64ede9959c9ca7a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 10:19:32 +0200 Subject: [PATCH 007/333] with windows shell prelaunch hook to found app hook and simplyfied it --- openpype/hooks/pre_foundry_apps.py | 28 +++++++++++++++ openpype/hooks/pre_with_windows_shell.py | 44 ------------------------ 2 files changed, 28 insertions(+), 44 deletions(-) create mode 100644 openpype/hooks/pre_foundry_apps.py delete mode 100644 openpype/hooks/pre_with_windows_shell.py diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py new file mode 100644 index 0000000000..85f68c6b60 --- /dev/null +++ b/openpype/hooks/pre_foundry_apps.py @@ -0,0 +1,28 @@ +import subprocess +from openpype.lib import PreLaunchHook + + +class LaunchFoundryAppsWindows(PreLaunchHook): + """Foundry applications have specific way how to launch them. + + Nuke is executed "like" python process so it is required to pass + `CREATE_NEW_CONSOLE` flag on windows to trigger creation of new console. + At the same time the newly created console won't create it's own stdout + and stderr handlers so they should not be redirected to DEVNULL. + """ + + # Should be as last hook because must change launch arguments to string + order = 1000 + app_groups = ["nuke", "nukex", "hiero", "nukestudio"] + platforms = ["windows"] + + def execute(self): + # Change `creationflags` to CREATE_NEW_CONSOLE + # - on Windows will nuke create new window using it's console + # Set `stdout` and `stderr` to None so new created console does not + # have redirected output to DEVNULL in build + self.launch_context.kwargs.update({ + "creationflags": subprocess.CREATE_NEW_CONSOLE, + "stdout": None, + "stderr": None + }) diff --git a/openpype/hooks/pre_with_windows_shell.py b/openpype/hooks/pre_with_windows_shell.py deleted file mode 100644 index 441ab1a675..0000000000 --- a/openpype/hooks/pre_with_windows_shell.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import subprocess -from openpype.lib import PreLaunchHook - - -class LaunchWithWindowsShell(PreLaunchHook): - """Add shell command before executable. - - Some hosts have issues when are launched directly from python in that case - it is possible to prepend shell executable which will trigger process - instead. - """ - - # Should be as last hook because must change launch arguments to string - order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio"] - platforms = ["windows"] - - def execute(self): - launch_args = self.launch_context.clear_launch_args( - self.launch_context.launch_args) - new_args = [ - # Get comspec which is cmd.exe in most cases. - os.environ.get("COMSPEC", "cmd.exe"), - # NOTE change to "/k" if want to keep console opened - "/c", - # Convert arguments to command line arguments (as string) - "\"{}\"".format( - subprocess.list2cmdline(launch_args) - ) - ] - # Convert list to string - # WARNING this only works if is used as string - args_string = " ".join(new_args) - self.log.info(( - "Modified launch arguments to be launched with shell \"{}\"." - ).format(args_string)) - - # Replace launch args with new one - self.launch_context.launch_args = args_string - # Change `creationflags` to CREATE_NEW_CONSOLE - self.launch_context.kwargs["creationflags"] = ( - subprocess.CREATE_NEW_CONSOLE - ) From b5900787079dd0f62c9a20911458a5d48ee1be6f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 16 Jun 2021 12:37:36 +0200 Subject: [PATCH 008/333] add PySide2 installation --- openpype/hosts/unreal/api/lib.py | 54 +++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index e069ac5256..56f92088b3 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -182,6 +182,36 @@ def create_unreal_project(project_name: str, """ env = env or os.environ preset = get_project_settings(project_name)["unreal"]["project_setup"] + ue_id = ".".join(ue_version.split(".")[:2]) + # get unreal engine identifier + # ------------------------------------------------------------------------- + # FIXME (antirotor): As of 4.26 this is problem with UE4 built from + # sources. In that case Engine ID is calculated per machine/user and not + # from Engine files as this code then reads. This then prevents UE4 + # to directly open project as it will complain about project being + # created in different UE4 version. When user convert such project + # to his UE4 version, Engine ID is replaced in uproject file. If some + # other user tries to open it, it will present him with similar error. + if platform.system().lower() == "windows": + ue4_modules = os.path.join(engine_path, "Engine", "Binaries", + "Win64", "UE4Editor.modules") + + if platform.system().lower() == "linux": + ue4_modules = os.path.join(engine_path, "Engine", "Binaries", + "Linux", "UE4Editor.modules") + + if platform.system().lower() == "darwin": + ue4_modules = os.path.join(engine_path, "Engine", "Binaries", + "Mac", "UE4Editor.modules") + + if os.path.exists(ue4_modules): + print("--- Loading Engine ID from modules file ...") + with open(ue4_modules, "r") as mp: + loaded_modules = json.load(mp) + + if loaded_modules.get("BuildId"): + ue_id = "{" + loaded_modules.get("BuildId") + "}" + plugins_path = None uep_path = None @@ -202,7 +232,7 @@ def create_unreal_project(project_name: str, # data for project file data = { "FileVersion": 3, - "EngineAssociation": ue_version, + "EngineAssociation": ue_id, "Category": "", "Description": "", "Plugins": [ @@ -305,6 +335,28 @@ def create_unreal_project(project_name: str, "pip", "install", "pyside"]) else: raise NotImplementedError("Unsupported platform") + else: + # install PySide2 inside newer engines + if platform.system().lower() == "windows": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python3", "Win64", + "python3.exe") + + if platform.system().lower() == "linux": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python3", "Linux", + "bin", "python3") + + if platform.system().lower() == "darwin": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python3", "Mac", + "bin", "python3") + + if python_path: + subprocess.run([python_path, "-m", + "pip", "install", "pyside2"]) + else: + raise NotImplementedError("Unsupported platform") if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) From cdbfe669c8df6b777e38cff15c71549d3d34c175 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 17 Jun 2021 13:47:23 +0200 Subject: [PATCH 009/333] raise exceptions when ue not found --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 7c4b6c3088..c292730fb1 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -4,7 +4,8 @@ import os from openpype.lib import ( PreLaunchHook, - ApplicationLaunchFailed + ApplicationLaunchFailed, + ApplicationNotFound ) from openpype.hosts.unreal.api import lib as unreal_lib @@ -62,6 +63,8 @@ class UnrealPrelaunchHook(PreLaunchHook): f"{self.signature} detected UE4 versions: " f"[ {detected_str} ]" )) + if not detected: + raise ApplicationNotFound("No Unreal Engines are found.") engine_version = ".".join(engine_version.split(".")[:2]) if engine_version not in detected.keys(): From b1e716d5cfeb7600f21287b48d57168a5b308775 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 12:19:41 +0200 Subject: [PATCH 010/333] StandalonePublisher: failing collector for editorial --- .../standalonepublisher/plugins/publish/collect_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py index eb04217136..ad89abba63 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py @@ -55,7 +55,7 @@ class CollectInstances(pyblish.api.InstancePlugin): fps = plib.get_asset()["data"]["fps"] tracks = timeline.each_child( - descended_from_type=otio.schema.track.Track + descended_from_type=otio.schema.Track ) # get data from avalon @@ -92,7 +92,7 @@ class CollectInstances(pyblish.api.InstancePlugin): # Transitions are ignored, because Clips have the full frame # range. - if isinstance(clip, otio.schema.transition.Transition): + if isinstance(clip, otio.schema.Transition): continue # basic unique asset name From 39076fac360e4906173384cbb884e63a70465a05 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Jun 2021 16:48:43 +0200 Subject: [PATCH 011/333] use pathlib, executable fixes --- openpype/hosts/unreal/api/lib.py | 173 +++++++++--------- .../unreal/hooks/pre_workfile_preparation.py | 29 ++- 2 files changed, 103 insertions(+), 99 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 56f92088b3..4760d1a78f 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -7,6 +7,7 @@ import json from distutils import dir_util import subprocess import re +from pathlib import Path from collections import OrderedDict from openpype.api import get_project_settings @@ -147,8 +148,8 @@ def _parse_launcher_locations(install_json_path: str) -> dict: def create_unreal_project(project_name: str, ue_version: str, - pr_dir: str, - engine_path: str, + pr_dir: Path, + engine_path: Path, dev_mode: bool = False, env: dict = None) -> None: """This will create `.uproject` file at specified location. @@ -162,8 +163,8 @@ def create_unreal_project(project_name: str, Args: project_name (str): Name of the project. ue_version (str): Unreal engine version (like 4.23). - pr_dir (str): Path to directory where project will be created. - engine_path (str): Path to Unreal Engine installation. + pr_dir (Path): Path to directory where project will be created. + engine_path (Path): Path to Unreal Engine installation. dev_mode (bool, optional): Flag to trigger C++ style Unreal project needing Visual Studio and other tools to compile plugins from sources. This will trigger automatically if `Binaries` @@ -192,19 +193,20 @@ def create_unreal_project(project_name: str, # created in different UE4 version. When user convert such project # to his UE4 version, Engine ID is replaced in uproject file. If some # other user tries to open it, it will present him with similar error. + ue4_modules = Path() if platform.system().lower() == "windows": - ue4_modules = os.path.join(engine_path, "Engine", "Binaries", - "Win64", "UE4Editor.modules") + ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + "Win64", "UE4Editor.modules")) if platform.system().lower() == "linux": - ue4_modules = os.path.join(engine_path, "Engine", "Binaries", - "Linux", "UE4Editor.modules") + ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + "Linux", "UE4Editor.modules")) if platform.system().lower() == "darwin": - ue4_modules = os.path.join(engine_path, "Engine", "Binaries", - "Mac", "UE4Editor.modules") + ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + "Mac", "UE4Editor.modules")) - if os.path.exists(ue4_modules): + if ue4_modules.exists(): print("--- Loading Engine ID from modules file ...") with open(ue4_modules, "r") as mp: loaded_modules = json.load(mp) @@ -217,16 +219,16 @@ def create_unreal_project(project_name: str, if os.path.isdir(env.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project - plugins_path = os.path.join(pr_dir, "Plugins") - avalon_plugin_path = os.path.join(plugins_path, "Avalon") - if not os.path.isdir(avalon_plugin_path): - os.makedirs(avalon_plugin_path, exist_ok=True) + plugins_path = pr_dir / "Plugins" + avalon_plugin_path = plugins_path / "Avalon" + if not avalon_plugin_path.is_dir(): + avalon_plugin_path.mkdir(parents=True, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), - avalon_plugin_path) + avalon_plugin_path.as_posix()) - if (not os.path.isdir(os.path.join(avalon_plugin_path, "Binaries")) - or not os.path.join(avalon_plugin_path, "Intermediate")): + if not (avalon_plugin_path / "Binaries").is_dir() \ + or not (avalon_plugin_path / "Intermediate").is_dir(): dev_mode = True # data for project file @@ -247,14 +249,14 @@ def create_unreal_project(project_name: str, # support offline installation. # Otherwise clone UnrealEnginePython to Plugins directory # https://github.com/20tab/UnrealEnginePython.git - uep_path = os.path.join(plugins_path, "UnrealEnginePython") + uep_path = plugins_path / "UnrealEnginePython" if env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): os.makedirs(uep_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree( env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), - uep_path) + uep_path.as_posix()) else: # WARNING: this will trigger dev_mode, because we need to compile # this plugin. @@ -262,13 +264,13 @@ def create_unreal_project(project_name: str, import git git.Repo.clone_from( "https://github.com/20tab/UnrealEnginePython.git", - uep_path) + uep_path.as_posix()) data["Plugins"].append( {"Name": "UnrealEnginePython", "Enabled": True}) - if (not os.path.isdir(os.path.join(uep_path, "Binaries")) - or not os.path.join(uep_path, "Intermediate")): + if not (uep_path / "Binaries").is_dir() \ + or not (uep_path / "Intermediate").is_dir(): dev_mode = True if dev_mode or preset["dev_mode"]: @@ -287,10 +289,8 @@ def create_unreal_project(project_name: str, # now we need to fix python path in: # `UnrealEnginePython.Build.cs` # to point to our python - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="r") as f: + with open(uep_path / "Source" / "UnrealEnginePython" / + "UnrealEnginePython.Build.cs", mode="r") as f: build_file = f.read() fix = build_file.replace( @@ -298,14 +298,12 @@ def create_unreal_project(project_name: str, 'private string pythonHome = "{}";'.format( sys.base_prefix.replace("\\", "/"))) - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="w") as f: + with open(uep_path / "Source" / "UnrealEnginePython" / + "UnrealEnginePython.Build.cs", mode="w") as f: f.write(fix) # write project file - project_file = os.path.join(pr_dir, "{}.uproject".format(project_name)) + project_file = pr_dir / f"{project_name}.uproject" with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) @@ -316,44 +314,43 @@ def create_unreal_project(project_name: str, if int(ue_version.split(".")[0]) == 4 and \ int(ue_version.split(".")[1]) < 25: if platform.system().lower() == "windows": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python", "Win64", - "python.exe") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python/Win64/python.exe") if platform.system().lower() == "linux": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python", "Linux", - "bin", "python") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python/Linux/bin/python") if platform.system().lower() == "darwin": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python", "Mac", - "bin", "python") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python/Mac/bin/python") - if python_path: - subprocess.run([python_path, "-m", + if python_path.exists(): + subprocess.run([python_path.as_posix(), "-m", "pip", "install", "pyside"]) else: raise NotImplementedError("Unsupported platform") else: # install PySide2 inside newer engines if platform.system().lower() == "windows": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python3", "Win64", - "python3.exe") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Win64/pythonw.exe") if platform.system().lower() == "linux": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python3", "Linux", - "bin", "python3") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Linux" / + "bin" / "python3") if platform.system().lower() == "darwin": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python3", "Mac", - "bin", "python3") + python_path = (engine_path / "Engine" / "Binaries" / + "ThirdParty" / "Python3" / "Mac" / + "bin" / "python3") if python_path: - subprocess.run([python_path, "-m", + if not python_path.exists(): + raise RuntimeError( + f"Unreal Python not found at {python_path}") + subprocess.run([python_path.as_posix(), "-m", "pip", "install", "pyside2"]) else: raise NotImplementedError("Unsupported platform") @@ -362,7 +359,7 @@ def create_unreal_project(project_name: str, _prepare_cpp_project(project_file, engine_path) -def _prepare_cpp_project(project_file: str, engine_path: str) -> None: +def _prepare_cpp_project(project_file: Path, engine_path: Path) -> None: """Prepare CPP Unreal Project. This function will add source files needed for project to be @@ -379,13 +376,13 @@ def _prepare_cpp_project(project_file: str, engine_path: str) -> None: engine_path (str): Path to unreal engine associated with project. """ - project_name = os.path.splitext(os.path.basename(project_file))[0] - project_dir = os.path.dirname(project_file) - targets_dir = os.path.join(project_dir, "Source") - sources_dir = os.path.join(targets_dir, project_name) + project_name = project_file.stem + project_dir = project_file.parent + targets_dir = project_dir / "Source" + sources_dir = targets_dir / project_name - os.makedirs(sources_dir, exist_ok=True) - os.makedirs(os.path.join(project_dir, "Content"), exist_ok=True) + sources_dir.mkdir(parents=True, exist_ok=True) + (project_dir / "Content").mkdir(parents=True, exist_ok=True) module_target = ''' using UnrealBuildTool; @@ -460,63 +457,59 @@ class {1}_API A{0}GameModeBase : public AGameModeBase }}; '''.format(project_name, project_name.upper()) - with open(os.path.join( - targets_dir, f"{project_name}.Target.cs"), mode="w") as f: + with open(targets_dir / f"{project_name}.Target.cs", mode="w") as f: f.write(module_target) - with open(os.path.join( - targets_dir, f"{project_name}Editor.Target.cs"), mode="w") as f: + with open(targets_dir / f"{project_name}Editor.Target.cs", mode="w") as f: f.write(editor_module_target) - with open(os.path.join( - sources_dir, f"{project_name}.Build.cs"), mode="w") as f: + with open(sources_dir / f"{project_name}.Build.cs", mode="w") as f: f.write(module_build) - with open(os.path.join( - sources_dir, f"{project_name}.cpp"), mode="w") as f: + with open(sources_dir / f"{project_name}.cpp", mode="w") as f: f.write(module_cpp) - with open(os.path.join( - sources_dir, f"{project_name}.h"), mode="w") as f: + with open(sources_dir / f"{project_name}.h", mode="w") as f: f.write(module_header) - with open(os.path.join( - sources_dir, f"{project_name}GameModeBase.cpp"), mode="w") as f: + with open(sources_dir / f"{project_name}GameModeBase.cpp", mode="w") as f: f.write(game_mode_cpp) - with open(os.path.join( - sources_dir, f"{project_name}GameModeBase.h"), mode="w") as f: + with open(sources_dir / f"{project_name}GameModeBase.h", mode="w") as f: f.write(game_mode_h) - u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" - "UnrealBuildTool.exe") + u_build_tool = Path( + engine_path / "Engine/Binaries/DotNET/UnrealBuildTool.exe") u_header_tool = None + arch = "Win64" if platform.system().lower() == "windows": - u_header_tool = (f"{engine_path}/Engine/Binaries/Win64/" - f"UnrealHeaderTool.exe") + arch = "Win64" + u_header_tool = Path( + engine_path / "Engine/Binaries/Win64/UnrealHeaderTool.exe") elif platform.system().lower() == "linux": - u_header_tool = (f"{engine_path}/Engine/Binaries/Linux/" - f"UnrealHeaderTool") + arch = "Linux" + u_header_tool = Path( + engine_path / "Engine/Binaries/Linux/UnrealHeaderTool") elif platform.system().lower() == "darwin": # we need to test this out - u_header_tool = (f"{engine_path}/Engine/Binaries/Mac/" - f"UnrealHeaderTool") + arch = "Mac" + u_header_tool = Path( + engine_path / "Engine/Binaries/Mac/UnrealHeaderTool") if not u_header_tool: raise NotImplementedError("Unsupported platform") - u_build_tool = u_build_tool.replace("\\", "/") - u_header_tool = u_header_tool.replace("\\", "/") - - command1 = [u_build_tool, "-projectfiles", f"-project={project_file}", - "-progress"] + command1 = [u_build_tool.as_posix(), "-projectfiles", + f"-project={project_file}", "-progress"] subprocess.run(command1) - command2 = [u_build_tool, f"-ModuleWithSuffix={project_name},3555" - "Win64", "Development", "-TargetType=Editor" - f'-Project="{project_file}"', f'"{project_file}"' + command2 = [u_build_tool.as_posix(), + f"-ModuleWithSuffix={project_name},3555", arch, + "Development", "-TargetType=Editor", + f'-Project={project_file}', + f'{project_file}', "-IgnoreJunk"] subprocess.run(command2) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index c292730fb1..2c4dd822bc 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Hook to launch Unreal and prepare projects.""" import os +from pathlib import Path +import platform from openpype.lib import ( PreLaunchHook, @@ -50,7 +52,7 @@ class UnrealPrelaunchHook(PreLaunchHook): )) unreal_project_name = f"P{unreal_project_name}" - project_path = os.path.join(workdir, unreal_project_name) + project_path = Path(os.path.join(workdir, unreal_project_name)) self.log.info(( f"{self.signature} requested UE4 version: " @@ -73,13 +75,21 @@ class UnrealPrelaunchHook(PreLaunchHook): f"detected [ {engine_version} ]" )) - os.makedirs(project_path, exist_ok=True) + ue4_path = Path(detected[engine_version]) / "Engine/Binaries" + if platform.system().lower() == "windows": + ue4_path = ue4_path / "Win64/UE4Editor.exe" - project_file = os.path.join( - project_path, - f"{unreal_project_name}.uproject" - ) - if not os.path.isfile(project_file): + elif platform.system().lower() == "linux": + ue4_path = ue4_path / "Linux/UE4Editor" + + elif platform.system().lower() == "darwin": + ue4_path = ue4_path / "Mac/UE4Editor" + + self.launch_context.launch_args.append(ue4_path.as_posix()) + project_path.mkdir(parents=True, exist_ok=True) + + project_file = project_path / f"{unreal_project_name}.uproject" + if not project_file.is_file(): engine_path = detected[engine_version] self.log.info(( f"{self.signature} creating unreal " @@ -95,8 +105,9 @@ class UnrealPrelaunchHook(PreLaunchHook): unreal_project_name, engine_version, project_path, - engine_path=engine_path + engine_path=Path(engine_path) ) # Append project file to launch arguments - self.launch_context.launch_args.append(f"\"{project_file}\"") + self.launch_context.launch_args.append( + f"\"{project_file.as_posix()}\"") From 3a2f29150b7b7f110c82990e9aa3e37b6793cc21 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Jun 2021 16:53:15 +0200 Subject: [PATCH 012/333] fix indents --- openpype/hosts/unreal/api/lib.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 4760d1a78f..8f05a63273 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -338,13 +338,11 @@ def create_unreal_project(project_name: str, if platform.system().lower() == "linux": python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Linux" / - "bin" / "python3") + "Python3/Linux/bin/python3") if platform.system().lower() == "darwin": - python_path = (engine_path / "Engine" / "Binaries" / - "ThirdParty" / "Python3" / "Mac" / - "bin" / "python3") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Mac/bin/python3") if python_path: if not python_path.exists(): From 313e433d726344fc2eabe6debb78ba8464f72dd2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Jun 2021 09:34:57 +0200 Subject: [PATCH 013/333] client#115 - added Texture batch for Standalone Publisher Added collector Added validator Added family --- .../plugins/publish/collect_texture.py | 220 ++++++++++++++++++ .../plugins/publish/validate_texture_batch.py | 47 ++++ .../project_settings/standalonepublisher.json | 34 ++- 3 files changed, 293 insertions(+), 8 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py new file mode 100644 index 0000000000..7b79fd1061 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -0,0 +1,220 @@ +import os +import copy +import re +import opentimelineio as otio +import pyblish.api +from openpype import lib as plib +import json + +class CollectTextures(pyblish.api.ContextPlugin): + """Collect workfile (and its resource_files) and textures.""" + + order = pyblish.api.CollectorOrder + label = "Collect Textures" + hosts = ["standalonepublisher"] + families = ["texture_batch"] + actions = [] + + main_workfile_extensions = ['mra'] + other_workfile_extensions = ['spp', 'psd'] + texture_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", + "gif", "svg"] + + color_space = ["lin_srgb", "raw", "acesg"] + + version_regex = re.compile(r"^(.+)_v([0-9]+)") + udim_regex = re.compile(r"_1[0-9]{3}\.") + + def process(self, context): + self.context = context + import json + def convertor(value): + return str(value) + + workfile_subset = "texturesMainWorkfile" + resource_files = {} + workfile_files = {} + representations = {} + version_data = {} + asset_builds = set() + asset = None + for instance in context: + if not asset: + asset = instance.data["asset"] # selected from SP + + self.log.info("instance.data:: {}".format( + json.dumps(instance.data, indent=4, default=convertor))) + processed_instance = False + for repre in instance.data["representations"]: + ext = repre["ext"].replace('.', '') + asset_build = version = None + if ext in self.main_workfile_extensions or \ + ext in self.other_workfile_extensions: + self.log.info('workfile') + asset_build, version = \ + self._parse_asset_build(repre["files"], + self.version_regex) + asset_builds.add((asset_build, version, + workfile_subset, 'workfile')) + processed_instance = True + + if not representations.get(workfile_subset): + representations[workfile_subset] = [] + + # asset_build must be here to tie workfile and texture + if not workfile_files.get(asset_build): + workfile_files[asset_build] = [] + + if ext in self.main_workfile_extensions: + representations[workfile_subset].append(repre) + workfile_files[asset_build].append(repre["files"]) + + if ext in self.other_workfile_extensions: + self.log.info("other") + # add only if not added already from main + if not representations.get(workfile_subset): + representations[workfile_subset].append(repre) + + if not workfile_files.get(asset_build): + workfile_files[asset_build].append(repre["files"]) + + if not resource_files.get(workfile_subset): + resource_files[workfile_subset] = [] + item = { + "files": [os.path.join(repre["stagingDir"], + repre["files"])], + "source": "standalone publisher" + } + resource_files[workfile_subset].append(item) + + if ext in self.texture_extensions: + c_space = self._get_color_space(repre["files"][0], + self.color_space) + subset = "texturesMain_{}".format(c_space) + + asset_build, version = \ + self._parse_asset_build(repre["files"][0], + self.version_regex) + + if not representations.get(subset): + representations[subset] = [] + representations[subset].append(repre) + + udim = self._parse_udim(repre["files"][0], self.udim_regex) + + if not version_data.get(subset): + version_data[subset] = [] + ver_data = { + "color_space": c_space, + "UDIM": udim, + } + version_data[subset].append(ver_data) + + asset_builds.add( + (asset_build, version, subset, "textures")) + processed_instance = True + + if processed_instance: + self.context.remove(instance) + + self.log.info("asset_builds:: {}".format(asset_builds)) + self._create_new_instances(context, + asset, + asset_builds, + resource_files, + representations, + version_data, + workfile_files) + + def _create_new_instances(self, context, asset, asset_builds, + resource_files, representations, + version_data, workfile_files): + """Prepare new instances from collected data. + + Args: + context (ContextPlugin) + asset (string): selected asset from SP + asset_builds (set) of tuples + (asset_build, version, subset, family) + resource_files (list) of resource dicts + representations (dict) of representation files, key is + asset_build + """ + for asset_build, version, subset, family in asset_builds: + + self.log.info("resources:: {}".format(resource_files)) + self.log.info("-"*25) + self.log.info("representations:: {}".format(representations)) + self.log.info("-"*25) + self.log.info("workfile_files:: {}".format(workfile_files)) + + new_instance = context.create_instance(subset) + new_instance.data.update( + { + "subset": subset, + "asset": asset, + "label": subset, + "name": subset, + "family": family, + "version": int(version), + "representations": representations.get(subset), + "families": [family] + } + ) + if resource_files.get(subset): + new_instance.data.update({ + "resources": resource_files.get(subset) + }) + + repre = representations.get(subset)[0] + new_instance.context.data["currentFile"] = os.path.join( + repre["stagingDir"], repre["files"][0]) + + ver_data = version_data.get(subset) + if ver_data: + ver_data = ver_data[0] + if workfile_files.get(asset_build): + ver_data['workfile'] = workfile_files.get(asset_build)[0] + + new_instance.data.update( + {"versionData": ver_data} + ) + + self.log.info("new instance:: {}".format(json.dumps(new_instance.data, indent=4))) + + def _parse_asset_build(self, name, version_regex): + regex_result = version_regex.findall(name) + asset_name = None # ?? + version_number = 1 + if regex_result: + asset_name, version_number = regex_result[0] + + return asset_name, version_number + + def _parse_udim(self, name, udim_regex): + regex_result = udim_regex.findall(name) + udim = None + if not regex_result: + self.log.warning("Didn't find UDIM in {}".format(name)) + else: + udim = re.sub("[^0-9]", '', regex_result[0]) + + return udim + + def _get_color_space(self, name, color_spaces): + """Looks for color_space from a list in a file name.""" + color_space = None + found = [cs for cs in color_spaces if + re.search("_{}_".format(cs), name)] + + if not found: + self.log.warning("No color space found in {}".format(name)) + else: + if len(found) > 1: + msg = "Multiple color spaces found in {}->{}".format(name, + found) + self.log.warning(msg) + + color_space = found[0] + + return color_space diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py new file mode 100644 index 0000000000..e222004456 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py @@ -0,0 +1,47 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureBatch(pyblish.api.ContextPlugin): + """Validates that collected instnaces for Texture batch are OK. + + Validates: + some textures are present + workfile has resource files (optional) + texture version matches to workfile version + """ + + label = "Validate Texture Batch" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["workfile", "textures"] + + def process(self, context): + + workfiles = [] + workfiles_in_textures = [] + for instance in context: + if instance.data["family"] == "workfile": + workfiles.append(instance.data["representations"][0]["files"]) + + if not instance.data.get("resources"): + msg = "No resources for workfile {}".\ + format(instance.data["name"]) + self.log.warning(msg) + + if instance.data["family"] == "textures": + wfile = instance.data["versionData"]["workfile"] + workfiles_in_textures.append(wfile) + + version_str = "v{:03d}".format(instance.data["version"]) + assert version_str in wfile, \ + "Not matching version, texture {} - workfile {}".format( + instance.data["version"], wfile + ) + + msg = "Not matching set of workfiles and textures." + \ + "{} not equal to {}".format(set(workfiles), + set(workfiles_in_textures)) +\ + "\nCheck that both workfile and textures are present" + keys = set(workfiles) == set(workfiles_in_textures) + assert keys, msg diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 7172612a74..5590fa6349 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -105,16 +105,33 @@ "label": "Render", "family": "render", "icon": "image", - "defaults": ["Animation", "Lighting", "Lookdev", "Compositing"], + "defaults": [ + "Animation", + "Lighting", + "Lookdev", + "Compositing" + ], "help": "Rendered images or video files" }, "create_mov_batch": { - "name": "mov_batch", - "label": "Batch Mov", - "family": "render_mov_batch", - "icon": "image", - "defaults": ["Main"], - "help": "Process multiple Mov files and publish them for layout and comp." + "name": "mov_batch", + "label": "Batch Mov", + "family": "render_mov_batch", + "icon": "image", + "defaults": [ + "Main" + ], + "help": "Process multiple Mov files and publish them for layout and comp." + }, + "create_texture_batch": { + "name": "texture_batch", + "label": "Texture Batch", + "family": "texture_batch", + "icon": "image", + "defaults": [ + "Main" + ], + "help": "Texture files with UDIM together with worfile" }, "__dynamic_keys_labels__": { "create_workfile": "Workfile", @@ -127,7 +144,8 @@ "create_image": "Image", "create_matchmove": "Matchmove", "create_render": "Render", - "create_mov_batch": "Batch Mov" + "create_mov_batch": "Batch Mov", + "create_texture_batch": "Batch Texture" } }, "publish": { From e8066f072e1d32787099879abf0639c0aa45e380 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 24 Jun 2021 13:03:43 +0200 Subject: [PATCH 014/333] add scriptsmenu module and basic Settings --- openpype/hosts/maya/api/menu.py | 4 +- .../defaults/project_settings/maya.json | 12 + .../projects_schema/schema_project_maya.json | 4 + .../schemas/schema_maya_scriptsmenu.json | 22 + .../python/common/scriptsmenu/__init__.py | 5 + .../python/common/scriptsmenu/action.py | 208 ++ .../common/scriptsmenu/launchformari.py | 54 + .../common/scriptsmenu/launchformaya.py | 137 ++ .../common/scriptsmenu/launchfornuke.py | 36 + .../python/common/scriptsmenu/scriptsmenu.py | 316 +++ .../python/common/scriptsmenu/vendor/Qt.py | 1989 +++++++++++++++++ .../common/scriptsmenu/vendor/__init__.py | 0 .../python/common/scriptsmenu/version.py | 9 + 13 files changed, 2794 insertions(+), 2 deletions(-) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json create mode 100644 openpype/vendor/python/common/scriptsmenu/__init__.py create mode 100644 openpype/vendor/python/common/scriptsmenu/action.py create mode 100644 openpype/vendor/python/common/scriptsmenu/launchformari.py create mode 100644 openpype/vendor/python/common/scriptsmenu/launchformaya.py create mode 100644 openpype/vendor/python/common/scriptsmenu/launchfornuke.py create mode 100644 openpype/vendor/python/common/scriptsmenu/scriptsmenu.py create mode 100644 openpype/vendor/python/common/scriptsmenu/vendor/Qt.py create mode 100644 openpype/vendor/python/common/scriptsmenu/vendor/__init__.py create mode 100644 openpype/vendor/python/common/scriptsmenu/version.py diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 42e5c66e4a..5e036b8e0c 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -83,7 +83,7 @@ def deferred(): if workfile_action: top_menu.removeAction(workfile_action) - log.info("Attempting to install scripts menu..") + log.info("Attempting to install scripts menu ...") add_build_workfiles_item() add_look_assigner_item() @@ -116,7 +116,7 @@ def deferred(): def uninstall(): menu = _get_menu() if menu: - log.info("Attempting to uninstall..") + log.info("Attempting to uninstall ...") try: menu.deleteLater() diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 284a1a0040..0375eb42d5 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -7,6 +7,18 @@ "workfile": "ma", "yetiRig": "ma" }, + "scriptsmenu": { + "name": "OpenPype Tools", + "definition": [ + { + "type": "action", + "command": "$OPENPYPE_SCRIPTS\\others\\save_scene_incremental.py", + "sourcetype": "file", + "title": "# Version Up", + "tooltip": "Incremental save with a specific format" + } + ] + }, "create": { "CreateLook": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 0a59cab510..c2a8274313 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -14,6 +14,10 @@ "type": "text" } }, + { + "type": "schema", + "name": "schema_maya_scriptsmenu" + }, { "type": "schema", "name": "schema_maya_create" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json new file mode 100644 index 0000000000..e841d6ba77 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_scriptsmenu.json @@ -0,0 +1,22 @@ +{ + "type": "dict", + "collapsible": true, + "key": "scriptsmenu", + "label": "Scripts Menu Definition", + "children": [ + { + "type": "text", + "key": "name", + "label": "Menu Name" + }, + { + "type": "splitter" + }, + { + "type": "raw-json", + "key": "definition", + "label": "Menu definition", + "is_list": true + } + ] +} \ No newline at end of file diff --git a/openpype/vendor/python/common/scriptsmenu/__init__.py b/openpype/vendor/python/common/scriptsmenu/__init__.py new file mode 100644 index 0000000000..a881f73533 --- /dev/null +++ b/openpype/vendor/python/common/scriptsmenu/__init__.py @@ -0,0 +1,5 @@ +from .scriptsmenu import ScriptsMenu +from . import version + +__all__ = ["ScriptsMenu"] +__version__ = version.version diff --git a/openpype/vendor/python/common/scriptsmenu/action.py b/openpype/vendor/python/common/scriptsmenu/action.py new file mode 100644 index 0000000000..5e68628406 --- /dev/null +++ b/openpype/vendor/python/common/scriptsmenu/action.py @@ -0,0 +1,208 @@ +import os + +from .vendor.Qt import QtWidgets + + +class Action(QtWidgets.QAction): + """Custom Action widget""" + + def __init__(self, parent=None): + + QtWidgets.QAction.__init__(self, parent) + + self._root = None + self._tags = list() + self._command = None + self._sourcetype = None + self._iconfile = None + self._label = None + + self._COMMAND = """import imp +f, filepath, descr = imp.find_module('{module_name}', ['{dirname}']) +module = imp.load_module('{module_name}', f, filepath, descr) +module.{module_name}()""" + + @property + def root(self): + return self._root + + @root.setter + def root(self, value): + self._root = value + + @property + def tags(self): + return self._tags + + @tags.setter + def tags(self, value): + self._tags = value + + @property + def command(self): + return self._command + + @command.setter + def command(self, value): + """ + Store the command in the QAction + + Args: + value (str): the full command which will be executed when clicked + + Return: + None + """ + self._command = value + + @property + def sourcetype(self): + return self._sourcetype + + @sourcetype.setter + def sourcetype(self, value): + """ + Set the command type to get the correct execution of the command given + + Args: + value (str): the name of the command type + + Returns: + None + + """ + self._sourcetype = value + + @property + def iconfile(self): + return self._iconfile + + @iconfile.setter + def iconfile(self, value): + """Store the path to the image file which needs to be displayed + + Args: + value (str): the path to the image + + Returns: + None + """ + self._iconfile = value + + @property + def label(self): + return self._label + + @label.setter + def label(self, value): + """ + Set the abbreviation which will be used as overlay text in the shelf + + Args: + value (str): an abbreviation of the name + + Returns: + None + + """ + self._label = value + + def run_command(self): + """ + Run the command of the instance or copy the command to the active shelf + based on the current modifiers. + + If callbacks have been registered with fouind modifier integer the + function will trigger all callbacks. When a callback function returns a + non zero integer it will not execute the action's command + + """ + + # get the current application and its linked keyboard modifiers + app = QtWidgets.QApplication.instance() + modifiers = app.keyboardModifiers() + + # If the menu has a callback registered for the current modifier + # we run the callback instead of the action itself. + registered = self._root.registered_callbacks + callbacks = registered.get(int(modifiers), []) + for callback in callbacks: + signal = callback(self) + if signal != 0: + # Exit function on non-zero return code + return + + exec(self.process_command()) + + def process_command(self): + """ + Check if the command is a file which needs to be launched and if it + has a relative path, if so return the full path by expanding + environment variables. Wrap any mel command in a executable string + for Python and return the string if the source type is + + Add your own source type and required process to ensure callback + is stored correctly. + + An example of a process is the sourcetype is MEL + (Maya Embedded Language) as Python cannot run it on its own so it + needs to be wrapped in a string in which we explicitly import mel and + run it as a mel.eval. The string is then parsed to python as + exec("command"). + + Returns: + str: a clean command which can be used + + """ + if self._sourcetype == "python": + return self._command + + if self._sourcetype == "mel": + # Escape single quotes + conversion = self._command.replace("'", "\\'") + return "import maya; maya.mel.eval('{}')".format(conversion) + + if self._sourcetype == "file": + if os.path.isabs(self._command): + filepath = self._command + else: + filepath = os.path.normpath(os.path.expandvars(self._command)) + + return self._wrap_filepath(filepath) + + def has_tag(self, tag): + """Check whether the tag matches with the action's tags. + + A partial match will also return True, for example tag `a` will match + correctly with the `ape` tag on the Action. + + Args: + tag (str): The tag + + Returns + bool: Whether the action is tagged with given tag + + """ + + for tagitem in self.tags: + if tag not in tagitem: + continue + return True + + return False + + def _wrap_filepath(self, file_path): + """Create a wrapped string for the python command + + Args: + file_path (str): the filepath of a script + + Returns: + str: the wrapped command + """ + + dirname = os.path.dirname(r"{}".format(file_path)) + dirpath = dirname.replace("\\", "/") + module_name = os.path.splitext(os.path.basename(file_path))[0] + + return self._COMMAND.format(module_name=module_name, dirname=dirpath) diff --git a/openpype/vendor/python/common/scriptsmenu/launchformari.py b/openpype/vendor/python/common/scriptsmenu/launchformari.py new file mode 100644 index 0000000000..25cfc80d96 --- /dev/null +++ b/openpype/vendor/python/common/scriptsmenu/launchformari.py @@ -0,0 +1,54 @@ + +# Import third-party modules +from vendor.Qt import QtWidgets + +# Import local modules +import scriptsmenu + + +def _mari_main_window(): + """Get Mari main window. + + Returns: + MriMainWindow: Mari's main window. + + """ + for obj in QtWidgets.QApplication.topLevelWidgets(): + if obj.metaObject().className() == 'MriMainWindow': + return obj + raise RuntimeError('Could not find Mari MainWindow instance') + + +def _mari_main_menubar(): + """Get Mari main menu bar. + + Returns: + Retrieve the main menubar of the Mari window. + + """ + mari_window = _mari_main_window() + menubar = [ + i for i in mari_window.children() if isinstance(i, QtWidgets.QMenuBar) + ] + assert len(menubar) == 1, "Error, could not find menu bar!" + return menubar[0] + + +def main(title="Scripts"): + """Build the main scripts menu in Mari. + + Args: + title (str): Name of the menu in the application. + + Returns: + scriptsmenu.ScriptsMenu: Instance object. + + """ + mari_main_bar = _mari_main_menubar() + for mari_bar in mari_main_bar.children(): + if isinstance(mari_bar, scriptsmenu.ScriptsMenu): + if mari_bar.title() == title: + menu = mari_bar + return menu + menu = scriptsmenu.ScriptsMenu(title=title, parent=mari_main_bar) + return menu diff --git a/openpype/vendor/python/common/scriptsmenu/launchformaya.py b/openpype/vendor/python/common/scriptsmenu/launchformaya.py new file mode 100644 index 0000000000..7ad66f0ad2 --- /dev/null +++ b/openpype/vendor/python/common/scriptsmenu/launchformaya.py @@ -0,0 +1,137 @@ +import logging + +import maya.cmds as cmds +import maya.mel as mel + +import scriptsmenu +from .vendor.Qt import QtCore, QtWidgets + +log = logging.getLogger(__name__) + + +def register_repeat_last(action): + """Register the action in repeatLast to ensure the RepeatLast hotkey works + + Args: + action (action.Action): Action wigdet instance + + Returns: + int: 0 + + """ + command = action.process_command() + command = command.replace("\n", "; ") + # Register command to Maya (mel) + cmds.repeatLast(addCommand='python("{}")'.format(command), + addCommandLabel=action.label) + + return 0 + + +def to_shelf(action): + """Copy clicked menu item to the currently active Maya shelf + Args: + action (action.Action): the action instance which is clicked + + Returns: + int: 1 + + """ + + shelftoplevel = mel.eval("$gShelfTopLevel = $gShelfTopLevel;") + current_active_shelf = cmds.tabLayout(shelftoplevel, + query=True, + selectTab=True) + + cmds.shelfButton(command=action.process_command(), + sourceType="python", + parent=current_active_shelf, + image=action.iconfile or "pythonFamily.png", + annotation=action.statusTip(), + imageOverlayLabel=action.label or "") + + return 1 + + +def _maya_main_window(): + """Return Maya's main window""" + for obj in QtWidgets.QApplication.topLevelWidgets(): + if obj.objectName() == 'MayaWindow': + return obj + raise RuntimeError('Could not find MayaWindow instance') + + +def _maya_main_menubar(): + """Retrieve the main menubar of the Maya window""" + mayawindow = _maya_main_window() + menubar = [i for i in mayawindow.children() + if isinstance(i, QtWidgets.QMenuBar)] + + assert len(menubar) == 1, "Error, could not find menu bar!" + + return menubar[0] + + +def find_scripts_menu(title, parent): + """ + Check if the menu exists with the given title in the parent + + Args: + title (str): the title name of the scripts menu + + parent (QtWidgets.QMenuBar): the menubar to check + + Returns: + QtWidgets.QMenu or None + + """ + + menu = None + search = [i for i in parent.children() if + isinstance(i, scriptsmenu.ScriptsMenu) + and i.title() == title] + + if search: + assert len(search) < 2, ("Multiple instances of menu '{}' " + "in menu bar".format(title)) + menu = search[0] + + return menu + + +def main(title="Scripts", parent=None, objectName=None): + """Build the main scripts menu in Maya + + Args: + title (str): name of the menu in the application + + parent (QtWidgets.QtMenuBar): the parent object for the menu + + objectName (str): custom objectName for scripts menu + + Returns: + scriptsmenu.ScriptsMenu instance + + """ + + mayamainbar = parent or _maya_main_menubar() + try: + # check menu already exists + menu = find_scripts_menu(title, mayamainbar) + if not menu: + log.info("Attempting to build menu ...") + object_name = objectName or title.lower() + menu = scriptsmenu.ScriptsMenu(title=title, + parent=mayamainbar, + objectName=object_name) + except Exception as e: + log.error(e) + return + + # Register control + shift callback to add to shelf (maya behavior) + modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier + menu.register_callback(int(modifiers), to_shelf) + + menu.register_callback(0, register_repeat_last) + + return menu diff --git a/openpype/vendor/python/common/scriptsmenu/launchfornuke.py b/openpype/vendor/python/common/scriptsmenu/launchfornuke.py new file mode 100644 index 0000000000..23e4ed1b4d --- /dev/null +++ b/openpype/vendor/python/common/scriptsmenu/launchfornuke.py @@ -0,0 +1,36 @@ +import scriptsmenu +from .vendor.Qt import QtWidgets + + +def _nuke_main_window(): + """Return Nuke's main window""" + for obj in QtWidgets.QApplication.topLevelWidgets(): + if (obj.inherits('QMainWindow') and + obj.metaObject().className() == 'Foundry::UI::DockMainWindow'): + return obj + raise RuntimeError('Could not find Nuke MainWindow instance') + + +def _nuke_main_menubar(): + """Retrieve the main menubar of the Nuke window""" + nuke_window = _nuke_main_window() + menubar = [i for i in nuke_window.children() + if isinstance(i, QtWidgets.QMenuBar)] + + assert len(menubar) == 1, "Error, could not find menu bar!" + return menubar[0] + + +def main(title="Scripts"): + # Register control + shift callback to add to shelf (Nuke behavior) + # modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier + # menu.register_callback(modifiers, to_shelf) + nuke_main_bar = _nuke_main_menubar() + for nuke_bar in nuke_main_bar.children(): + if isinstance(nuke_bar, scriptsmenu.ScriptsMenu): + if nuke_bar.title() == title: + menu = nuke_bar + return menu + + menu = scriptsmenu.ScriptsMenu(title=title, parent=nuke_main_bar) + return menu \ No newline at end of file diff --git a/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py new file mode 100644 index 0000000000..e2b7ff96c7 --- /dev/null +++ b/openpype/vendor/python/common/scriptsmenu/scriptsmenu.py @@ -0,0 +1,316 @@ +import os +import json +import logging +from collections import defaultdict + +from .vendor.Qt import QtWidgets, QtCore +from . import action + +log = logging.getLogger(__name__) + + +class ScriptsMenu(QtWidgets.QMenu): + """A Qt menu that displays a list of searchable actions""" + + updated = QtCore.Signal(QtWidgets.QMenu) + + def __init__(self, *args, **kwargs): + """Initialize Scripts menu + + Args: + title (str): the name of the root menu which will be created + + parent (QtWidgets.QObject) : the QObject to parent the menu to + + Returns: + None + + """ + QtWidgets.QMenu.__init__(self, *args, **kwargs) + + self.searchbar = None + self.update_action = None + + self._script_actions = [] + self._callbacks = defaultdict(list) + + # Automatically add it to the parent menu + parent = kwargs.get("parent", None) + if parent: + parent.addMenu(self) + + objectname = kwargs.get("objectName", "scripts") + title = kwargs.get("title", "Scripts") + self.setObjectName(objectname) + self.setTitle(title) + + # add default items in the menu + self.create_default_items() + + def on_update(self): + self.updated.emit(self) + + @property + def registered_callbacks(self): + return self._callbacks.copy() + + def create_default_items(self): + """Add a search bar to the top of the menu given""" + + # create widget and link function + searchbar = QtWidgets.QLineEdit() + searchbar.setFixedWidth(120) + searchbar.setPlaceholderText("Search ...") + searchbar.textChanged.connect(self._update_search) + self.searchbar = searchbar + + # create widget holder + searchbar_action = QtWidgets.QWidgetAction(self) + + # add widget to widget holder + searchbar_action.setDefaultWidget(self.searchbar) + searchbar_action.setObjectName("Searchbar") + + # add update button and link function + update_action = QtWidgets.QAction(self) + update_action.setObjectName("Update Scripts") + update_action.setText("Update Scripts") + update_action.setVisible(False) + update_action.triggered.connect(self.on_update) + self.update_action = update_action + + # add action to menu + self.addAction(searchbar_action) + self.addAction(update_action) + + # add separator object + separator = self.addSeparator() + separator.setObjectName("separator") + + def add_menu(self, title, parent=None): + """Create a sub menu for a parent widget + + Args: + parent(QtWidgets.QWidget): the object to parent the menu to + + title(str): the title of the menu + + Returns: + QtWidget.QMenu instance + """ + + if not parent: + parent = self + + menu = QtWidgets.QMenu(parent, title) + menu.setTitle(title) + menu.setObjectName(title) + menu.setTearOffEnabled(True) + parent.addMenu(menu) + + return menu + + def add_script(self, parent, title, command, sourcetype, icon=None, + tags=None, label=None, tooltip=None): + """Create an action item which runs a script when clicked + + Args: + parent (QtWidget.QWidget): The widget to parent the item to + + title (str): The text which will be displayed in the menu + + command (str): The command which needs to be run when the item is + clicked. + + sourcetype (str): The type of command, the way the command is + processed is based on the source type. + + icon (str): The file path of an icon to display with the menu item + + tags (list, tuple): Keywords which describe the action + + label (str): A short description of the script which will be displayed + when hovering over the menu item + + tooltip (str): A tip for the user about the usage fo the tool + + Returns: + QtWidget.QAction instance + + """ + + assert tags is None or isinstance(tags, (list, tuple)) + # Ensure tags is a list + tags = list() if tags is None else list(tags) + tags.append(title.lower()) + + assert icon is None or isinstance(icon, str), ( + "Invalid data type for icon, supported : None, string") + + # create new action + script_action = action.Action(parent) + script_action.setText(title) + script_action.setObjectName(title) + script_action.tags = tags + + # link action to root for callback library + script_action.root = self + + # Set up the command + script_action.sourcetype = sourcetype + script_action.command = command + + try: + script_action.process_command() + except RuntimeError as e: + raise RuntimeError("Script action can't be " + "processed: {}".format(e)) + + if icon: + iconfile = os.path.expandvars(icon) + script_action.iconfile = iconfile + script_action_icon = QtWidgets.QIcon(iconfile) + script_action.setIcon(script_action_icon) + + if label: + script_action.label = label + + if tooltip: + script_action.setStatusTip(tooltip) + + script_action.triggered.connect(script_action.run_command) + parent.addAction(script_action) + + # Add to our searchable actions + self._script_actions.append(script_action) + + return script_action + + def build_from_configuration(self, parent, configuration): + """Process the configurations and store the configuration + + This creates all submenus from a configuration.json file. + + When the configuration holds the key `main` all scripts under `main` will + be added to the main menu first before adding the rest + + Args: + parent (ScriptsMenu): script menu instance + configuration (list): A ScriptsMenu configuration list + + Returns: + None + + """ + + for item in configuration: + assert isinstance(item, dict), "Configuration is wrong!" + + # skip items which have no `type` key + item_type = item.get('type', None) + if not item_type: + log.warning("Missing 'type' from configuration item") + continue + + # add separator + # Special behavior for separators + if item_type == "separator": + parent.addSeparator() + + # add submenu + # items should hold a collection of submenu items (dict) + elif item_type == "menu": + assert "items" in item, "Menu is missing 'items' key" + menu = self.add_menu(parent=parent, title=item["title"]) + self.build_from_configuration(menu, item["items"]) + + # add script + elif item_type == "action": + # filter out `type` from the item dict + config = {key: value for key, value in + item.items() if key != "type"} + + self.add_script(parent=parent, **config) + + def set_update_visible(self, state): + self.update_action.setVisible(state) + + def clear_menu(self): + """Clear all menu items which are not default + + Returns: + None + + """ + + # TODO: Set up a more robust implementation for this + # Delete all except the first three actions + for _action in self.actions()[3:]: + self.removeAction(_action) + + def register_callback(self, modifiers, callback): + self._callbacks[modifiers].append(callback) + + def _update_search(self, search): + """Hide all the samples which do not match the user's import + + Returns: + None + + """ + + if not search: + for action in self._script_actions: + action.setVisible(True) + else: + for action in self._script_actions: + if not action.has_tag(search.lower()): + action.setVisible(False) + + # Set visibility for all submenus + for action in self.actions(): + if not action.menu(): + continue + + menu = action.menu() + visible = any(action.isVisible() for action in menu.actions()) + action.setVisible(visible) + + +def load_configuration(path): + """Load the configuration from a file + + Read out the JSON file which will dictate the structure of the scripts menu + + Args: + path (str): file path of the .JSON file + + Returns: + dict + + """ + + if not os.path.isfile(path): + raise AttributeError("Given configuration is not " + "a file!\n'{}'".format(path)) + + extension = os.path.splitext(path)[-1] + if extension != ".json": + raise AttributeError("Given configuration file has unsupported " + "file type, provide a .json file") + + # retrieve and store config + with open(path, "r") as f: + configuration = json.load(f) + + return configuration + + +def application(configuration, parent): + import sys + app = QtWidgets.QApplication(sys.argv) + + scriptsmenu = ScriptsMenu(configuration, parent) + scriptsmenu.show() + + sys.exit(app.exec_()) diff --git a/openpype/vendor/python/common/scriptsmenu/vendor/Qt.py b/openpype/vendor/python/common/scriptsmenu/vendor/Qt.py new file mode 100644 index 0000000000..fe4b45f18f --- /dev/null +++ b/openpype/vendor/python/common/scriptsmenu/vendor/Qt.py @@ -0,0 +1,1989 @@ +"""Minimal Python 2 & 3 shim around all Qt bindings + +DOCUMENTATION + Qt.py was born in the film and visual effects industry to address + the growing need for the development of software capable of running + with more than one flavour of the Qt bindings for Python - PySide, + PySide2, PyQt4 and PyQt5. + + 1. Build for one, run with all + 2. Explicit is better than implicit + 3. Support co-existence + + Default resolution order: + - PySide2 + - PyQt5 + - PySide + - PyQt4 + + Usage: + >> import sys + >> from Qt import QtWidgets + >> app = QtWidgets.QApplication(sys.argv) + >> button = QtWidgets.QPushButton("Hello World") + >> button.show() + >> app.exec_() + + All members of PySide2 are mapped from other bindings, should they exist. + If no equivalent member exist, it is excluded from Qt.py and inaccessible. + The idea is to highlight members that exist across all supported binding, + and guarantee that code that runs on one binding runs on all others. + + For more details, visit https://github.com/mottosso/Qt.py + +LICENSE + + See end of file for license (MIT, BSD) information. + +""" + +import os +import sys +import types +import shutil +import importlib + + +__version__ = "1.2.3" + +# Enable support for `from Qt import *` +__all__ = [] + +# Flags from environment variables +QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) +QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") +QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") + +# Reference to Qt.py +Qt = sys.modules[__name__] +Qt.QtCompat = types.ModuleType("QtCompat") + +try: + long +except NameError: + # Python 3 compatibility + long = int + + +"""Common members of all bindings + +This is where each member of Qt.py is explicitly defined. +It is based on a "lowest common denominator" of all bindings; +including members found in each of the 4 bindings. + +The "_common_members" dictionary is generated using the +build_membership.sh script. + +""" + +_common_members = { + "QtCore": [ + "QAbstractAnimation", + "QAbstractEventDispatcher", + "QAbstractItemModel", + "QAbstractListModel", + "QAbstractState", + "QAbstractTableModel", + "QAbstractTransition", + "QAnimationGroup", + "QBasicTimer", + "QBitArray", + "QBuffer", + "QByteArray", + "QByteArrayMatcher", + "QChildEvent", + "QCoreApplication", + "QCryptographicHash", + "QDataStream", + "QDate", + "QDateTime", + "QDir", + "QDirIterator", + "QDynamicPropertyChangeEvent", + "QEasingCurve", + "QElapsedTimer", + "QEvent", + "QEventLoop", + "QEventTransition", + "QFile", + "QFileInfo", + "QFileSystemWatcher", + "QFinalState", + "QGenericArgument", + "QGenericReturnArgument", + "QHistoryState", + "QItemSelectionRange", + "QIODevice", + "QLibraryInfo", + "QLine", + "QLineF", + "QLocale", + "QMargins", + "QMetaClassInfo", + "QMetaEnum", + "QMetaMethod", + "QMetaObject", + "QMetaProperty", + "QMimeData", + "QModelIndex", + "QMutex", + "QMutexLocker", + "QObject", + "QParallelAnimationGroup", + "QPauseAnimation", + "QPersistentModelIndex", + "QPluginLoader", + "QPoint", + "QPointF", + "QProcess", + "QProcessEnvironment", + "QPropertyAnimation", + "QReadLocker", + "QReadWriteLock", + "QRect", + "QRectF", + "QRegExp", + "QResource", + "QRunnable", + "QSemaphore", + "QSequentialAnimationGroup", + "QSettings", + "QSignalMapper", + "QSignalTransition", + "QSize", + "QSizeF", + "QSocketNotifier", + "QState", + "QStateMachine", + "QSysInfo", + "QSystemSemaphore", + "QT_TRANSLATE_NOOP", + "QT_TR_NOOP", + "QT_TR_NOOP_UTF8", + "QTemporaryFile", + "QTextBoundaryFinder", + "QTextCodec", + "QTextDecoder", + "QTextEncoder", + "QTextStream", + "QTextStreamManipulator", + "QThread", + "QThreadPool", + "QTime", + "QTimeLine", + "QTimer", + "QTimerEvent", + "QTranslator", + "QUrl", + "QVariantAnimation", + "QWaitCondition", + "QWriteLocker", + "QXmlStreamAttribute", + "QXmlStreamAttributes", + "QXmlStreamEntityDeclaration", + "QXmlStreamEntityResolver", + "QXmlStreamNamespaceDeclaration", + "QXmlStreamNotationDeclaration", + "QXmlStreamReader", + "QXmlStreamWriter", + "Qt", + "QtCriticalMsg", + "QtDebugMsg", + "QtFatalMsg", + "QtMsgType", + "QtSystemMsg", + "QtWarningMsg", + "qAbs", + "qAddPostRoutine", + "qChecksum", + "qCritical", + "qDebug", + "qFatal", + "qFuzzyCompare", + "qIsFinite", + "qIsInf", + "qIsNaN", + "qIsNull", + "qRegisterResourceData", + "qUnregisterResourceData", + "qVersion", + "qWarning", + "qrand", + "qsrand" + ], + "QtGui": [ + "QAbstractTextDocumentLayout", + "QActionEvent", + "QBitmap", + "QBrush", + "QClipboard", + "QCloseEvent", + "QColor", + "QConicalGradient", + "QContextMenuEvent", + "QCursor", + "QDesktopServices", + "QDoubleValidator", + "QDrag", + "QDragEnterEvent", + "QDragLeaveEvent", + "QDragMoveEvent", + "QDropEvent", + "QFileOpenEvent", + "QFocusEvent", + "QFont", + "QFontDatabase", + "QFontInfo", + "QFontMetrics", + "QFontMetricsF", + "QGradient", + "QHelpEvent", + "QHideEvent", + "QHoverEvent", + "QIcon", + "QIconDragEvent", + "QIconEngine", + "QImage", + "QImageIOHandler", + "QImageReader", + "QImageWriter", + "QInputEvent", + "QInputMethodEvent", + "QIntValidator", + "QKeyEvent", + "QKeySequence", + "QLinearGradient", + "QMatrix2x2", + "QMatrix2x3", + "QMatrix2x4", + "QMatrix3x2", + "QMatrix3x3", + "QMatrix3x4", + "QMatrix4x2", + "QMatrix4x3", + "QMatrix4x4", + "QMouseEvent", + "QMoveEvent", + "QMovie", + "QPaintDevice", + "QPaintEngine", + "QPaintEngineState", + "QPaintEvent", + "QPainter", + "QPainterPath", + "QPainterPathStroker", + "QPalette", + "QPen", + "QPicture", + "QPictureIO", + "QPixmap", + "QPixmapCache", + "QPolygon", + "QPolygonF", + "QQuaternion", + "QRadialGradient", + "QRegExpValidator", + "QRegion", + "QResizeEvent", + "QSessionManager", + "QShortcutEvent", + "QShowEvent", + "QStandardItem", + "QStandardItemModel", + "QStatusTipEvent", + "QSyntaxHighlighter", + "QTabletEvent", + "QTextBlock", + "QTextBlockFormat", + "QTextBlockGroup", + "QTextBlockUserData", + "QTextCharFormat", + "QTextCursor", + "QTextDocument", + "QTextDocumentFragment", + "QTextFormat", + "QTextFragment", + "QTextFrame", + "QTextFrameFormat", + "QTextImageFormat", + "QTextInlineObject", + "QTextItem", + "QTextLayout", + "QTextLength", + "QTextLine", + "QTextList", + "QTextListFormat", + "QTextObject", + "QTextObjectInterface", + "QTextOption", + "QTextTable", + "QTextTableCell", + "QTextTableCellFormat", + "QTextTableFormat", + "QTouchEvent", + "QTransform", + "QValidator", + "QVector2D", + "QVector3D", + "QVector4D", + "QWhatsThisClickedEvent", + "QWheelEvent", + "QWindowStateChangeEvent", + "qAlpha", + "qBlue", + "qGray", + "qGreen", + "qIsGray", + "qRed", + "qRgb", + "qRgba" + ], + "QtHelp": [ + "QHelpContentItem", + "QHelpContentModel", + "QHelpContentWidget", + "QHelpEngine", + "QHelpEngineCore", + "QHelpIndexModel", + "QHelpIndexWidget", + "QHelpSearchEngine", + "QHelpSearchQuery", + "QHelpSearchQueryWidget", + "QHelpSearchResultWidget" + ], + "QtMultimedia": [ + "QAbstractVideoBuffer", + "QAbstractVideoSurface", + "QAudio", + "QAudioDeviceInfo", + "QAudioFormat", + "QAudioInput", + "QAudioOutput", + "QVideoFrame", + "QVideoSurfaceFormat" + ], + "QtNetwork": [ + "QAbstractNetworkCache", + "QAbstractSocket", + "QAuthenticator", + "QHostAddress", + "QHostInfo", + "QLocalServer", + "QLocalSocket", + "QNetworkAccessManager", + "QNetworkAddressEntry", + "QNetworkCacheMetaData", + "QNetworkConfiguration", + "QNetworkConfigurationManager", + "QNetworkCookie", + "QNetworkCookieJar", + "QNetworkDiskCache", + "QNetworkInterface", + "QNetworkProxy", + "QNetworkProxyFactory", + "QNetworkProxyQuery", + "QNetworkReply", + "QNetworkRequest", + "QNetworkSession", + "QSsl", + "QTcpServer", + "QTcpSocket", + "QUdpSocket" + ], + "QtOpenGL": [ + "QGL", + "QGLContext", + "QGLFormat", + "QGLWidget" + ], + "QtPrintSupport": [ + "QAbstractPrintDialog", + "QPageSetupDialog", + "QPrintDialog", + "QPrintEngine", + "QPrintPreviewDialog", + "QPrintPreviewWidget", + "QPrinter", + "QPrinterInfo" + ], + "QtSql": [ + "QSql", + "QSqlDatabase", + "QSqlDriver", + "QSqlDriverCreatorBase", + "QSqlError", + "QSqlField", + "QSqlIndex", + "QSqlQuery", + "QSqlQueryModel", + "QSqlRecord", + "QSqlRelation", + "QSqlRelationalDelegate", + "QSqlRelationalTableModel", + "QSqlResult", + "QSqlTableModel" + ], + "QtSvg": [ + "QGraphicsSvgItem", + "QSvgGenerator", + "QSvgRenderer", + "QSvgWidget" + ], + "QtTest": [ + "QTest" + ], + "QtWidgets": [ + "QAbstractButton", + "QAbstractGraphicsShapeItem", + "QAbstractItemDelegate", + "QAbstractItemView", + "QAbstractScrollArea", + "QAbstractSlider", + "QAbstractSpinBox", + "QAction", + "QActionGroup", + "QApplication", + "QBoxLayout", + "QButtonGroup", + "QCalendarWidget", + "QCheckBox", + "QColorDialog", + "QColumnView", + "QComboBox", + "QCommandLinkButton", + "QCommonStyle", + "QCompleter", + "QDataWidgetMapper", + "QDateEdit", + "QDateTimeEdit", + "QDesktopWidget", + "QDial", + "QDialog", + "QDialogButtonBox", + "QDirModel", + "QDockWidget", + "QDoubleSpinBox", + "QErrorMessage", + "QFileDialog", + "QFileIconProvider", + "QFileSystemModel", + "QFocusFrame", + "QFontComboBox", + "QFontDialog", + "QFormLayout", + "QFrame", + "QGesture", + "QGestureEvent", + "QGestureRecognizer", + "QGraphicsAnchor", + "QGraphicsAnchorLayout", + "QGraphicsBlurEffect", + "QGraphicsColorizeEffect", + "QGraphicsDropShadowEffect", + "QGraphicsEffect", + "QGraphicsEllipseItem", + "QGraphicsGridLayout", + "QGraphicsItem", + "QGraphicsItemGroup", + "QGraphicsLayout", + "QGraphicsLayoutItem", + "QGraphicsLineItem", + "QGraphicsLinearLayout", + "QGraphicsObject", + "QGraphicsOpacityEffect", + "QGraphicsPathItem", + "QGraphicsPixmapItem", + "QGraphicsPolygonItem", + "QGraphicsProxyWidget", + "QGraphicsRectItem", + "QGraphicsRotation", + "QGraphicsScale", + "QGraphicsScene", + "QGraphicsSceneContextMenuEvent", + "QGraphicsSceneDragDropEvent", + "QGraphicsSceneEvent", + "QGraphicsSceneHelpEvent", + "QGraphicsSceneHoverEvent", + "QGraphicsSceneMouseEvent", + "QGraphicsSceneMoveEvent", + "QGraphicsSceneResizeEvent", + "QGraphicsSceneWheelEvent", + "QGraphicsSimpleTextItem", + "QGraphicsTextItem", + "QGraphicsTransform", + "QGraphicsView", + "QGraphicsWidget", + "QGridLayout", + "QGroupBox", + "QHBoxLayout", + "QHeaderView", + "QInputDialog", + "QItemDelegate", + "QItemEditorCreatorBase", + "QItemEditorFactory", + "QKeyEventTransition", + "QLCDNumber", + "QLabel", + "QLayout", + "QLayoutItem", + "QLineEdit", + "QListView", + "QListWidget", + "QListWidgetItem", + "QMainWindow", + "QMdiArea", + "QMdiSubWindow", + "QMenu", + "QMenuBar", + "QMessageBox", + "QMouseEventTransition", + "QPanGesture", + "QPinchGesture", + "QPlainTextDocumentLayout", + "QPlainTextEdit", + "QProgressBar", + "QProgressDialog", + "QPushButton", + "QRadioButton", + "QRubberBand", + "QScrollArea", + "QScrollBar", + "QShortcut", + "QSizeGrip", + "QSizePolicy", + "QSlider", + "QSpacerItem", + "QSpinBox", + "QSplashScreen", + "QSplitter", + "QSplitterHandle", + "QStackedLayout", + "QStackedWidget", + "QStatusBar", + "QStyle", + "QStyleFactory", + "QStyleHintReturn", + "QStyleHintReturnMask", + "QStyleHintReturnVariant", + "QStyleOption", + "QStyleOptionButton", + "QStyleOptionComboBox", + "QStyleOptionComplex", + "QStyleOptionDockWidget", + "QStyleOptionFocusRect", + "QStyleOptionFrame", + "QStyleOptionGraphicsItem", + "QStyleOptionGroupBox", + "QStyleOptionHeader", + "QStyleOptionMenuItem", + "QStyleOptionProgressBar", + "QStyleOptionRubberBand", + "QStyleOptionSizeGrip", + "QStyleOptionSlider", + "QStyleOptionSpinBox", + "QStyleOptionTab", + "QStyleOptionTabBarBase", + "QStyleOptionTabWidgetFrame", + "QStyleOptionTitleBar", + "QStyleOptionToolBar", + "QStyleOptionToolBox", + "QStyleOptionToolButton", + "QStyleOptionViewItem", + "QStylePainter", + "QStyledItemDelegate", + "QSwipeGesture", + "QSystemTrayIcon", + "QTabBar", + "QTabWidget", + "QTableView", + "QTableWidget", + "QTableWidgetItem", + "QTableWidgetSelectionRange", + "QTapAndHoldGesture", + "QTapGesture", + "QTextBrowser", + "QTextEdit", + "QTimeEdit", + "QToolBar", + "QToolBox", + "QToolButton", + "QToolTip", + "QTreeView", + "QTreeWidget", + "QTreeWidgetItem", + "QTreeWidgetItemIterator", + "QUndoCommand", + "QUndoGroup", + "QUndoStack", + "QUndoView", + "QVBoxLayout", + "QWhatsThis", + "QWidget", + "QWidgetAction", + "QWidgetItem", + "QWizard", + "QWizardPage" + ], + "QtX11Extras": [ + "QX11Info" + ], + "QtXml": [ + "QDomAttr", + "QDomCDATASection", + "QDomCharacterData", + "QDomComment", + "QDomDocument", + "QDomDocumentFragment", + "QDomDocumentType", + "QDomElement", + "QDomEntity", + "QDomEntityReference", + "QDomImplementation", + "QDomNamedNodeMap", + "QDomNode", + "QDomNodeList", + "QDomNotation", + "QDomProcessingInstruction", + "QDomText", + "QXmlAttributes", + "QXmlContentHandler", + "QXmlDTDHandler", + "QXmlDeclHandler", + "QXmlDefaultHandler", + "QXmlEntityResolver", + "QXmlErrorHandler", + "QXmlInputSource", + "QXmlLexicalHandler", + "QXmlLocator", + "QXmlNamespaceSupport", + "QXmlParseException", + "QXmlReader", + "QXmlSimpleReader" + ], + "QtXmlPatterns": [ + "QAbstractMessageHandler", + "QAbstractUriResolver", + "QAbstractXmlNodeModel", + "QAbstractXmlReceiver", + "QSourceLocation", + "QXmlFormatter", + "QXmlItem", + "QXmlName", + "QXmlNamePool", + "QXmlNodeModelIndex", + "QXmlQuery", + "QXmlResultItems", + "QXmlSchema", + "QXmlSchemaValidator", + "QXmlSerializer" + ] +} + +""" Missing members + +This mapping describes members that have been deprecated +in one or more bindings and have been left out of the +_common_members mapping. + +The member can provide an extra details string to be +included in exceptions and warnings. +""" + +_missing_members = { + "QtGui": { + "QMatrix": "Deprecated in PyQt5", + }, +} + + +def _qInstallMessageHandler(handler): + """Install a message handler that works in all bindings + + Args: + handler: A function that takes 3 arguments, or None + """ + def messageOutputHandler(*args): + # In Qt4 bindings, message handlers are passed 2 arguments + # In Qt5 bindings, message handlers are passed 3 arguments + # The first argument is a QtMsgType + # The last argument is the message to be printed + # The Middle argument (if passed) is a QMessageLogContext + if len(args) == 3: + msgType, logContext, msg = args + elif len(args) == 2: + msgType, msg = args + logContext = None + else: + raise TypeError( + "handler expected 2 or 3 arguments, got {0}".format(len(args))) + + if isinstance(msg, bytes): + # In python 3, some bindings pass a bytestring, which cannot be + # used elsewhere. Decoding a python 2 or 3 bytestring object will + # consistently return a unicode object. + msg = msg.decode() + + handler(msgType, logContext, msg) + + passObject = messageOutputHandler if handler else handler + if Qt.IsPySide or Qt.IsPyQt4: + return Qt._QtCore.qInstallMsgHandler(passObject) + elif Qt.IsPySide2 or Qt.IsPyQt5: + return Qt._QtCore.qInstallMessageHandler(passObject) + + +def _getcpppointer(object): + if hasattr(Qt, "_shiboken2"): + return getattr(Qt, "_shiboken2").getCppPointer(object)[0] + elif hasattr(Qt, "_shiboken"): + return getattr(Qt, "_shiboken").getCppPointer(object)[0] + elif hasattr(Qt, "_sip"): + return getattr(Qt, "_sip").unwrapinstance(object) + raise AttributeError("'module' has no attribute 'getCppPointer'") + + +def _wrapinstance(ptr, base=None): + """Enable implicit cast of pointer to most suitable class + + This behaviour is available in sip per default. + + Based on http://nathanhorne.com/pyqtpyside-wrap-instance + + Usage: + This mechanism kicks in under these circumstances. + 1. Qt.py is using PySide 1 or 2. + 2. A `base` argument is not provided. + + See :func:`QtCompat.wrapInstance()` + + Arguments: + ptr (long): Pointer to QObject in memory + base (QObject, optional): Base class to wrap with. Defaults to QObject, + which should handle anything. + + """ + + assert isinstance(ptr, long), "Argument 'ptr' must be of type " + assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( + "Argument 'base' must be of type ") + + if Qt.IsPyQt4 or Qt.IsPyQt5: + func = getattr(Qt, "_sip").wrapinstance + elif Qt.IsPySide2: + func = getattr(Qt, "_shiboken2").wrapInstance + elif Qt.IsPySide: + func = getattr(Qt, "_shiboken").wrapInstance + else: + raise AttributeError("'module' has no attribute 'wrapInstance'") + + if base is None: + q_object = func(long(ptr), Qt.QtCore.QObject) + meta_object = q_object.metaObject() + class_name = meta_object.className() + super_class_name = meta_object.superClass().className() + + if hasattr(Qt.QtWidgets, class_name): + base = getattr(Qt.QtWidgets, class_name) + + elif hasattr(Qt.QtWidgets, super_class_name): + base = getattr(Qt.QtWidgets, super_class_name) + + else: + base = Qt.QtCore.QObject + + return func(long(ptr), base) + + +def _isvalid(object): + """Check if the object is valid to use in Python runtime. + + Usage: + See :func:`QtCompat.isValid()` + + Arguments: + object (QObject): QObject to check the validity of. + + """ + + assert isinstance(object, Qt.QtCore.QObject) + + if hasattr(Qt, "_shiboken2"): + return getattr(Qt, "_shiboken2").isValid(object) + + elif hasattr(Qt, "_shiboken"): + return getattr(Qt, "_shiboken").isValid(object) + + elif hasattr(Qt, "_sip"): + return not getattr(Qt, "_sip").isdeleted(object) + + else: + raise AttributeError("'module' has no attribute isValid") + + +def _translate(context, sourceText, *args): + # In Qt4 bindings, translate can be passed 2 or 3 arguments + # In Qt5 bindings, translate can be passed 2 arguments + # The first argument is disambiguation[str] + # The last argument is n[int] + # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] + if len(args) == 3: + disambiguation, encoding, n = args + elif len(args) == 2: + disambiguation, n = args + encoding = None + else: + raise TypeError( + "Expected 4 or 5 arguments, got {0}.".format(len(args) + 2)) + + if hasattr(Qt.QtCore, "QCoreApplication"): + app = getattr(Qt.QtCore, "QCoreApplication") + else: + raise NotImplementedError( + "Missing QCoreApplication implementation for {binding}".format( + binding=Qt.__binding__, + ) + ) + if Qt.__binding__ in ("PySide2", "PyQt5"): + sanitized_args = [context, sourceText, disambiguation, n] + else: + sanitized_args = [ + context, + sourceText, + disambiguation, + encoding or app.CodecForTr, + n + ] + return app.translate(*sanitized_args) + + +def _loadUi(uifile, baseinstance=None): + """Dynamically load a user interface from the given `uifile` + + This function calls `uic.loadUi` if using PyQt bindings, + else it implements a comparable binding for PySide. + + Documentation: + http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi + + Arguments: + uifile (str): Absolute path to Qt Designer file. + baseinstance (QWidget): Instantiated QWidget or subclass thereof + + Return: + baseinstance if `baseinstance` is not `None`. Otherwise + return the newly created instance of the user interface. + + """ + if hasattr(Qt, "_uic"): + return Qt._uic.loadUi(uifile, baseinstance) + + elif hasattr(Qt, "_QtUiTools"): + # Implement `PyQt5.uic.loadUi` for PySide(2) + + class _UiLoader(Qt._QtUiTools.QUiLoader): + """Create the user interface in a base instance. + + Unlike `Qt._QtUiTools.QUiLoader` itself this class does not + create a new instance of the top-level widget, but creates the user + interface in an existing instance of the top-level class if needed. + + This mimics the behaviour of `PyQt5.uic.loadUi`. + + """ + + def __init__(self, baseinstance): + super(_UiLoader, self).__init__(baseinstance) + self.baseinstance = baseinstance + self.custom_widgets = {} + + def _loadCustomWidgets(self, etree): + """ + Workaround to pyside-77 bug. + + From QUiLoader doc we should use registerCustomWidget method. + But this causes a segfault on some platforms. + + Instead we fetch from customwidgets DOM node the python class + objects. Then we can directly use them in createWidget method. + """ + + def headerToModule(header): + """ + Translate a header file to python module path + foo/bar.h => foo.bar + """ + # Remove header extension + module = os.path.splitext(header)[0] + + # Replace os separator by python module separator + return module.replace("/", ".").replace("\\", ".") + + custom_widgets = etree.find("customwidgets") + + if custom_widgets is None: + return + + for custom_widget in custom_widgets: + class_name = custom_widget.find("class").text + header = custom_widget.find("header").text + module = importlib.import_module(headerToModule(header)) + self.custom_widgets[class_name] = getattr(module, + class_name) + + def load(self, uifile, *args, **kwargs): + from xml.etree.ElementTree import ElementTree + + # For whatever reason, if this doesn't happen then + # reading an invalid or non-existing .ui file throws + # a RuntimeError. + etree = ElementTree() + etree.parse(uifile) + self._loadCustomWidgets(etree) + + widget = Qt._QtUiTools.QUiLoader.load( + self, uifile, *args, **kwargs) + + # Workaround for PySide 1.0.9, see issue #208 + widget.parentWidget() + + return widget + + def createWidget(self, class_name, parent=None, name=""): + """Called for each widget defined in ui file + + Overridden here to populate `baseinstance` instead. + + """ + + if parent is None and self.baseinstance: + # Supposed to create the top-level widget, + # return the base instance instead + return self.baseinstance + + # For some reason, Line is not in the list of available + # widgets, but works fine, so we have to special case it here. + if class_name in self.availableWidgets() + ["Line"]: + # Create a new widget for child widgets + widget = Qt._QtUiTools.QUiLoader.createWidget(self, + class_name, + parent, + name) + elif class_name in self.custom_widgets: + widget = self.custom_widgets[class_name](parent) + else: + raise Exception("Custom widget '%s' not supported" + % class_name) + + if self.baseinstance: + # Set an attribute for the new child widget on the base + # instance, just like PyQt5.uic.loadUi does. + setattr(self.baseinstance, name, widget) + + return widget + + widget = _UiLoader(baseinstance).load(uifile) + Qt.QtCore.QMetaObject.connectSlotsByName(widget) + + return widget + + else: + raise NotImplementedError("No implementation available for loadUi") + + +"""Misplaced members + +These members from the original submodule are misplaced relative PySide2 + +""" +_misplaced_members = { + "PySide2": { + "QtCore.QStringListModel": "QtCore.QStringListModel", + "QtGui.QStringListModel": "QtCore.QStringListModel", + "QtCore.Property": "QtCore.Property", + "QtCore.Signal": "QtCore.Signal", + "QtCore.Slot": "QtCore.Slot", + "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", + "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", + "QtCore.QItemSelection": "QtCore.QItemSelection", + "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", + "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", + "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], + "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], + "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer], + "shiboken2.isValid": ["QtCompat.isValid", _isvalid], + "QtWidgets.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtWidgets.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMessageHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", + }, + "PyQt5": { + "QtCore.pyqtProperty": "QtCore.Property", + "QtCore.pyqtSignal": "QtCore.Signal", + "QtCore.pyqtSlot": "QtCore.Slot", + "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", + "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", + "QtCore.QStringListModel": "QtCore.QStringListModel", + "QtCore.QItemSelection": "QtCore.QItemSelection", + "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", + "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", + "uic.loadUi": ["QtCompat.loadUi", _loadUi], + "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], + "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], + "sip.isdeleted": ["QtCompat.isValid", _isvalid], + "QtWidgets.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtWidgets.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMessageHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", + }, + "PySide": { + "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", + "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", + "QtGui.QStringListModel": "QtCore.QStringListModel", + "QtGui.QItemSelection": "QtCore.QItemSelection", + "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", + "QtCore.Property": "QtCore.Property", + "QtCore.Signal": "QtCore.Signal", + "QtCore.Slot": "QtCore.Slot", + "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", + "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", + "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", + "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", + "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", + "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", + "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", + "QtGui.QPrinter": "QtPrintSupport.QPrinter", + "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", + "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], + "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], + "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer], + "shiboken.isValid": ["QtCompat.isValid", _isvalid], + "QtGui.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtGui.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMsgHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4", + }, + "PyQt4": { + "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", + "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", + "QtGui.QItemSelection": "QtCore.QItemSelection", + "QtGui.QStringListModel": "QtCore.QStringListModel", + "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", + "QtCore.pyqtProperty": "QtCore.Property", + "QtCore.pyqtSignal": "QtCore.Signal", + "QtCore.pyqtSlot": "QtCore.Slot", + "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", + "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", + "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", + "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", + "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", + "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", + "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", + "QtGui.QPrinter": "QtPrintSupport.QPrinter", + "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", + # "QtCore.pyqtSignature": "QtCore.Slot", + "uic.loadUi": ["QtCompat.loadUi", _loadUi], + "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], + "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], + "sip.isdeleted": ["QtCompat.isValid", _isvalid], + "QtCore.QString": "str", + "QtGui.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtGui.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMsgHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4", + } +} + +""" Compatibility Members + +This dictionary is used to build Qt.QtCompat objects that provide a consistent +interface for obsolete members, and differences in binding return values. + +{ + "binding": { + "classname": { + "targetname": "binding_namespace", + } + } +} +""" +_compatibility_members = { + "PySide2": { + "QWidget": { + "grab": "QtWidgets.QWidget.grab", + }, + "QHeaderView": { + "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", + "setSectionsClickable": + "QtWidgets.QHeaderView.setSectionsClickable", + "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", + "setSectionResizeMode": + "QtWidgets.QHeaderView.setSectionResizeMode", + "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", + "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", + }, + "QFileDialog": { + "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", + "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", + "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", + }, + }, + "PyQt5": { + "QWidget": { + "grab": "QtWidgets.QWidget.grab", + }, + "QHeaderView": { + "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", + "setSectionsClickable": + "QtWidgets.QHeaderView.setSectionsClickable", + "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", + "setSectionResizeMode": + "QtWidgets.QHeaderView.setSectionResizeMode", + "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", + "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", + }, + "QFileDialog": { + "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", + "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", + "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", + }, + }, + "PySide": { + "QWidget": { + "grab": "QtWidgets.QPixmap.grabWidget", + }, + "QHeaderView": { + "sectionsClickable": "QtWidgets.QHeaderView.isClickable", + "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", + "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", + "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", + "sectionsMovable": "QtWidgets.QHeaderView.isMovable", + "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", + }, + "QFileDialog": { + "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", + "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", + "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", + }, + }, + "PyQt4": { + "QWidget": { + "grab": "QtWidgets.QPixmap.grabWidget", + }, + "QHeaderView": { + "sectionsClickable": "QtWidgets.QHeaderView.isClickable", + "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", + "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", + "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", + "sectionsMovable": "QtWidgets.QHeaderView.isMovable", + "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", + }, + "QFileDialog": { + "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", + "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", + "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", + }, + }, +} + + +def _apply_site_config(): + try: + import QtSiteConfig + except ImportError: + # If no QtSiteConfig module found, no modifications + # to _common_members are needed. + pass + else: + # Provide the ability to modify the dicts used to build Qt.py + if hasattr(QtSiteConfig, 'update_members'): + QtSiteConfig.update_members(_common_members) + + if hasattr(QtSiteConfig, 'update_misplaced_members'): + QtSiteConfig.update_misplaced_members(members=_misplaced_members) + + if hasattr(QtSiteConfig, 'update_compatibility_members'): + QtSiteConfig.update_compatibility_members( + members=_compatibility_members) + + +def _new_module(name): + return types.ModuleType(__name__ + "." + name) + + +def _import_sub_module(module, name): + """import_sub_module will mimic the function of importlib.import_module""" + module = __import__(module.__name__ + "." + name) + for level in name.split("."): + module = getattr(module, level) + return module + + +def _setup(module, extras): + """Install common submodules""" + + Qt.__binding__ = module.__name__ + + for name in list(_common_members) + extras: + try: + submodule = _import_sub_module( + module, name) + except ImportError: + try: + # For extra modules like sip and shiboken that may not be + # children of the binding. + submodule = __import__(name) + except ImportError: + continue + + setattr(Qt, "_" + name, submodule) + + if name not in extras: + # Store reference to original binding, + # but don't store speciality modules + # such as uic or QtUiTools + setattr(Qt, name, _new_module(name)) + + +def _reassign_misplaced_members(binding): + """Apply misplaced members from `binding` to Qt.py + + Arguments: + binding (dict): Misplaced members + + """ + + for src, dst in _misplaced_members[binding].items(): + dst_value = None + + src_parts = src.split(".") + src_module = src_parts[0] + src_member = None + if len(src_parts) > 1: + src_member = src_parts[1:] + + if isinstance(dst, (list, tuple)): + dst, dst_value = dst + + dst_parts = dst.split(".") + dst_module = dst_parts[0] + dst_member = None + if len(dst_parts) > 1: + dst_member = dst_parts[1] + + # Get the member we want to store in the namesapce. + if not dst_value: + try: + _part = getattr(Qt, "_" + src_module) + while src_member: + member = src_member.pop(0) + _part = getattr(_part, member) + dst_value = _part + except AttributeError: + # If the member we want to store in the namespace does not + # exist, there is no need to continue. This can happen if a + # request was made to rename a member that didn't exist, for + # example if QtWidgets isn't available on the target platform. + _log("Misplaced member has no source: {0}".format(src)) + continue + + try: + src_object = getattr(Qt, dst_module) + except AttributeError: + if dst_module not in _common_members: + # Only create the Qt parent module if its listed in + # _common_members. Without this check, if you remove QtCore + # from _common_members, the default _misplaced_members will add + # Qt.QtCore so it can add Signal, Slot, etc. + msg = 'Not creating missing member module "{m}" for "{c}"' + _log(msg.format(m=dst_module, c=dst_member)) + continue + # If the dst is valid but the Qt parent module does not exist + # then go ahead and create a new module to contain the member. + setattr(Qt, dst_module, _new_module(dst_module)) + src_object = getattr(Qt, dst_module) + # Enable direct import of the new module + sys.modules[__name__ + "." + dst_module] = src_object + + if not dst_value: + dst_value = getattr(Qt, "_" + src_module) + if src_member: + dst_value = getattr(dst_value, src_member) + + setattr( + src_object, + dst_member or dst_module, + dst_value + ) + + +def _build_compatibility_members(binding, decorators=None): + """Apply `binding` to QtCompat + + Arguments: + binding (str): Top level binding in _compatibility_members. + decorators (dict, optional): Provides the ability to decorate the + original Qt methods when needed by a binding. This can be used + to change the returned value to a standard value. The key should + be the classname, the value is a dict where the keys are the + target method names, and the values are the decorator functions. + + """ + + decorators = decorators or dict() + + # Allow optional site-level customization of the compatibility members. + # This method does not need to be implemented in QtSiteConfig. + try: + import QtSiteConfig + except ImportError: + pass + else: + if hasattr(QtSiteConfig, 'update_compatibility_decorators'): + QtSiteConfig.update_compatibility_decorators(binding, decorators) + + _QtCompat = type("QtCompat", (object,), {}) + + for classname, bindings in _compatibility_members[binding].items(): + attrs = {} + for target, binding in bindings.items(): + namespaces = binding.split('.') + try: + src_object = getattr(Qt, "_" + namespaces[0]) + except AttributeError as e: + _log("QtCompat: AttributeError: %s" % e) + # Skip reassignment of non-existing members. + # This can happen if a request was made to + # rename a member that didn't exist, for example + # if QtWidgets isn't available on the target platform. + continue + + # Walk down any remaining namespace getting the object assuming + # that if the first namespace exists the rest will exist. + for namespace in namespaces[1:]: + src_object = getattr(src_object, namespace) + + # decorate the Qt method if a decorator was provided. + if target in decorators.get(classname, []): + # staticmethod must be called on the decorated method to + # prevent a TypeError being raised when the decorated method + # is called. + src_object = staticmethod( + decorators[classname][target](src_object)) + + attrs[target] = src_object + + # Create the QtCompat class and install it into the namespace + compat_class = type(classname, (_QtCompat,), attrs) + setattr(Qt.QtCompat, classname, compat_class) + + +def _pyside2(): + """Initialise PySide2 + + These functions serve to test the existence of a binding + along with set it up in such a way that it aligns with + the final step; adding members from the original binding + to Qt.py + + """ + + import PySide2 as module + extras = ["QtUiTools"] + try: + try: + # Before merge of PySide and shiboken + import shiboken2 + except ImportError: + # After merge of PySide and shiboken, May 2017 + from PySide2 import shiboken2 + extras.append("shiboken2") + except ImportError: + pass + + _setup(module, extras) + Qt.__binding_version__ = module.__version__ + + if hasattr(Qt, "_shiboken2"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + Qt.QtCompat.delete = shiboken2.delete + + if hasattr(Qt, "_QtUiTools"): + Qt.QtCompat.loadUi = _loadUi + + if hasattr(Qt, "_QtCore"): + Qt.__qt_version__ = Qt._QtCore.qVersion() + Qt.QtCompat.dataChanged = ( + lambda self, topleft, bottomright, roles=None: + self.dataChanged.emit(topleft, bottomright, roles or []) + ) + + if hasattr(Qt, "_QtWidgets"): + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtWidgets.QHeaderView.setSectionResizeMode + + _reassign_misplaced_members("PySide2") + _build_compatibility_members("PySide2") + + +def _pyside(): + """Initialise PySide""" + + import PySide as module + extras = ["QtUiTools"] + try: + try: + # Before merge of PySide and shiboken + import shiboken + except ImportError: + # After merge of PySide and shiboken, May 2017 + from PySide import shiboken + extras.append("shiboken") + except ImportError: + pass + + _setup(module, extras) + Qt.__binding_version__ = module.__version__ + + if hasattr(Qt, "_shiboken"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + Qt.QtCompat.delete = shiboken.delete + + if hasattr(Qt, "_QtUiTools"): + Qt.QtCompat.loadUi = _loadUi + + if hasattr(Qt, "_QtGui"): + setattr(Qt, "QtWidgets", _new_module("QtWidgets")) + setattr(Qt, "_QtWidgets", Qt._QtGui) + if hasattr(Qt._QtGui, "QX11Info"): + setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) + Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info + + Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode + + if hasattr(Qt, "_QtCore"): + Qt.__qt_version__ = Qt._QtCore.qVersion() + Qt.QtCompat.dataChanged = ( + lambda self, topleft, bottomright, roles=None: + self.dataChanged.emit(topleft, bottomright) + ) + + _reassign_misplaced_members("PySide") + _build_compatibility_members("PySide") + + +def _pyqt5(): + """Initialise PyQt5""" + + import PyQt5 as module + extras = ["uic"] + + try: + import sip + extras += ["sip"] + except ImportError: + + # Relevant to PyQt5 5.11 and above + try: + from PyQt5 import sip + extras += ["sip"] + except ImportError: + sip = None + + _setup(module, extras) + if hasattr(Qt, "_sip"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + Qt.QtCompat.delete = sip.delete + + if hasattr(Qt, "_uic"): + Qt.QtCompat.loadUi = _loadUi + + if hasattr(Qt, "_QtCore"): + Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR + Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR + Qt.QtCompat.dataChanged = ( + lambda self, topleft, bottomright, roles=None: + self.dataChanged.emit(topleft, bottomright, roles or []) + ) + + if hasattr(Qt, "_QtWidgets"): + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtWidgets.QHeaderView.setSectionResizeMode + + _reassign_misplaced_members("PyQt5") + _build_compatibility_members('PyQt5') + + +def _pyqt4(): + """Initialise PyQt4""" + + import sip + + # Validation of envivornment variable. Prevents an error if + # the variable is invalid since it's just a hint. + try: + hint = int(QT_SIP_API_HINT) + except TypeError: + hint = None # Variable was None, i.e. not set. + except ValueError: + raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2") + + for api in ("QString", + "QVariant", + "QDate", + "QDateTime", + "QTextStream", + "QTime", + "QUrl"): + try: + sip.setapi(api, hint or 2) + except AttributeError: + raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py") + except ValueError: + actual = sip.getapi(api) + if not hint: + raise ImportError("API version already set to %d" % actual) + else: + # Having provided a hint indicates a soft constraint, one + # that doesn't throw an exception. + sys.stderr.write( + "Warning: API '%s' has already been set to %d.\n" + % (api, actual) + ) + + import PyQt4 as module + extras = ["uic"] + try: + import sip + extras.append(sip.__name__) + except ImportError: + sip = None + + _setup(module, extras) + if hasattr(Qt, "_sip"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + Qt.QtCompat.delete = sip.delete + + if hasattr(Qt, "_uic"): + Qt.QtCompat.loadUi = _loadUi + + if hasattr(Qt, "_QtGui"): + setattr(Qt, "QtWidgets", _new_module("QtWidgets")) + setattr(Qt, "_QtWidgets", Qt._QtGui) + if hasattr(Qt._QtGui, "QX11Info"): + setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) + Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info + + Qt.QtCompat.setSectionResizeMode = \ + Qt._QtGui.QHeaderView.setResizeMode + + if hasattr(Qt, "_QtCore"): + Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR + Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR + Qt.QtCompat.dataChanged = ( + lambda self, topleft, bottomright, roles=None: + self.dataChanged.emit(topleft, bottomright) + ) + + _reassign_misplaced_members("PyQt4") + + # QFileDialog QtCompat decorator + def _standardizeQFileDialog(some_function): + """Decorator that makes PyQt4 return conform to other bindings""" + def wrapper(*args, **kwargs): + ret = (some_function(*args, **kwargs)) + + # PyQt4 only returns the selected filename, force it to a + # standard return of the selected filename, and a empty string + # for the selected filter + return ret, '' + + wrapper.__doc__ = some_function.__doc__ + wrapper.__name__ = some_function.__name__ + + return wrapper + + decorators = { + "QFileDialog": { + "getOpenFileName": _standardizeQFileDialog, + "getOpenFileNames": _standardizeQFileDialog, + "getSaveFileName": _standardizeQFileDialog, + } + } + _build_compatibility_members('PyQt4', decorators) + + +def _none(): + """Internal option (used in installer)""" + + Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None}) + + Qt.__binding__ = "None" + Qt.__qt_version__ = "0.0.0" + Qt.__binding_version__ = "0.0.0" + Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None + Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None + + for submodule in _common_members.keys(): + setattr(Qt, submodule, Mock()) + setattr(Qt, "_" + submodule, Mock()) + + +def _log(text): + if QT_VERBOSE: + sys.stdout.write(text + "\n") + + +def _convert(lines): + """Convert compiled .ui file from PySide2 to Qt.py + + Arguments: + lines (list): Each line of of .ui file + + Usage: + >> with open("myui.py") as f: + .. lines = _convert(f.readlines()) + + """ + + def parse(line): + line = line.replace("from PySide2 import", "from Qt import QtCompat,") + line = line.replace("QtWidgets.QApplication.translate", + "QtCompat.translate") + if "QtCore.SIGNAL" in line: + raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 " + "and so Qt.py does not support it: you " + "should avoid defining signals inside " + "your ui files.") + return line + + parsed = list() + for line in lines: + line = parse(line) + parsed.append(line) + + return parsed + + +def _cli(args): + """Qt.py command-line interface""" + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--convert", + help="Path to compiled Python module, e.g. my_ui.py") + parser.add_argument("--compile", + help="Accept raw .ui file and compile with native " + "PySide2 compiler.") + parser.add_argument("--stdout", + help="Write to stdout instead of file", + action="store_true") + parser.add_argument("--stdin", + help="Read from stdin instead of file", + action="store_true") + + args = parser.parse_args(args) + + if args.stdout: + raise NotImplementedError("--stdout") + + if args.stdin: + raise NotImplementedError("--stdin") + + if args.compile: + raise NotImplementedError("--compile") + + if args.convert: + sys.stdout.write("#\n" + "# WARNING: --convert is an ALPHA feature.\n#\n" + "# See https://github.com/mottosso/Qt.py/pull/132\n" + "# for details.\n" + "#\n") + + # + # ------> Read + # + with open(args.convert) as f: + lines = _convert(f.readlines()) + + backup = "%s_backup%s" % os.path.splitext(args.convert) + sys.stdout.write("Creating \"%s\"..\n" % backup) + shutil.copy(args.convert, backup) + + # + # <------ Write + # + with open(args.convert, "w") as f: + f.write("".join(lines)) + + sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) + + +class MissingMember(object): + """ + A placeholder type for a missing Qt object not + included in Qt.py + + Args: + name (str): The name of the missing type + details (str): An optional custom error message + """ + ERR_TMPL = ("{} is not a common object across PySide2 " + "and the other Qt bindings. It is not included " + "as a common member in the Qt.py layer") + + def __init__(self, name, details=''): + self.__name = name + self.__err = self.ERR_TMPL.format(name) + + if details: + self.__err = "{}: {}".format(self.__err, details) + + def __repr__(self): + return "<{}: {}>".format(self.__class__.__name__, self.__name) + + def __getattr__(self, name): + raise NotImplementedError(self.__err) + + def __call__(self, *a, **kw): + raise NotImplementedError(self.__err) + + +def _install(): + # Default order (customise order and content via QT_PREFERRED_BINDING) + default_order = ("PySide2", "PyQt5", "PySide", "PyQt4") + preferred_order = list( + b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b + ) + + order = preferred_order or default_order + + available = { + "PySide2": _pyside2, + "PyQt5": _pyqt5, + "PySide": _pyside, + "PyQt4": _pyqt4, + "None": _none + } + + _log("Order: '%s'" % "', '".join(order)) + + # Allow site-level customization of the available modules. + _apply_site_config() + + found_binding = False + for name in order: + _log("Trying %s" % name) + + try: + available[name]() + found_binding = True + break + + except ImportError as e: + _log("ImportError: %s" % e) + + except KeyError: + _log("ImportError: Preferred binding '%s' not found." % name) + + if not found_binding: + # If not binding were found, throw this error + raise ImportError("No Qt binding were found.") + + # Install individual members + for name, members in _common_members.items(): + try: + their_submodule = getattr(Qt, "_%s" % name) + except AttributeError: + continue + + our_submodule = getattr(Qt, name) + + # Enable import * + __all__.append(name) + + # Enable direct import of submodule, + # e.g. import Qt.QtCore + sys.modules[__name__ + "." + name] = our_submodule + + for member in members: + # Accept that a submodule may miss certain members. + try: + their_member = getattr(their_submodule, member) + except AttributeError: + _log("'%s.%s' was missing." % (name, member)) + continue + + setattr(our_submodule, member, their_member) + + # Install missing member placeholders + for name, members in _missing_members.items(): + our_submodule = getattr(Qt, name) + + for member in members: + + # If the submodule already has this member installed, + # either by the common members, or the site config, + # then skip installing this one over it. + if hasattr(our_submodule, member): + continue + + placeholder = MissingMember("{}.{}".format(name, member), + details=members[member]) + setattr(our_submodule, member, placeholder) + + # Enable direct import of QtCompat + sys.modules['Qt.QtCompat'] = Qt.QtCompat + + # Backwards compatibility + if hasattr(Qt.QtCompat, 'loadUi'): + Qt.QtCompat.load_ui = Qt.QtCompat.loadUi + + +_install() + +# Setup Binding Enum states +Qt.IsPySide2 = Qt.__binding__ == 'PySide2' +Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5' +Qt.IsPySide = Qt.__binding__ == 'PySide' +Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4' + +"""Augment QtCompat + +QtCompat contains wrappers and added functionality +to the original bindings, such as the CLI interface +and otherwise incompatible members between bindings, +such as `QHeaderView.setSectionResizeMode`. + +""" + +Qt.QtCompat._cli = _cli +Qt.QtCompat._convert = _convert + +# Enable command-line interface +if __name__ == "__main__": + _cli(sys.argv[1:]) + + +# The MIT License (MIT) +# +# Copyright (c) 2016-2017 Marcus Ottosson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# In PySide(2), loadUi does not exist, so we implement it +# +# `_UiLoader` is adapted from the qtpy project, which was further influenced +# by qt-helpers which was released under a 3-clause BSD license which in turn +# is based on a solution at: +# +# - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 +# +# The License for this code is as follows: +# +# qt-helpers - a common front-end to various Qt modules +# +# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# * Neither the name of the Glue project nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Which itself was based on the solution at +# +# https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 +# +# which was released under the MIT license: +# +# Copyright (c) 2011 Sebastian Wiesner +# Modifications by Charl Botha +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files +# (the "Software"),to deal in the Software without restriction, +# including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/openpype/vendor/python/common/scriptsmenu/vendor/__init__.py b/openpype/vendor/python/common/scriptsmenu/vendor/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/vendor/python/common/scriptsmenu/version.py b/openpype/vendor/python/common/scriptsmenu/version.py new file mode 100644 index 0000000000..73f9426c2d --- /dev/null +++ b/openpype/vendor/python/common/scriptsmenu/version.py @@ -0,0 +1,9 @@ +VERSION_MAJOR = 1 +VERSION_MINOR = 5 +VERSION_PATCH = 1 + + +version = '{}.{}.{}'.format(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) +__version__ = version + +__all__ = ['version', '__version__'] From c916d128bc05dcd2f99e01a1015901a875947226 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 24 Jun 2021 13:12:36 +0200 Subject: [PATCH 015/333] mark support for Unreal Python Engine deprecated --- openpype/hosts/unreal/api/lib.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 8f05a63273..6231fd6f33 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -245,17 +245,21 @@ def create_unreal_project(project_name: str, } if preset["install_unreal_python_engine"]: - # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to - # support offline installation. + # WARNING: This is deprecated as Unreal Engine Python project + # is on hold and is mainly replaced in 4.26 by Epics own + # Python implementation. + # --------------------------------------------------------------- + # If `OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from + # there to support offline installation. # Otherwise clone UnrealEnginePython to Plugins directory # https://github.com/20tab/UnrealEnginePython.git uep_path = plugins_path / "UnrealEnginePython" - if env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + if env.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): os.makedirs(uep_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree( - env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + env.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), uep_path.as_posix()) else: # WARNING: this will trigger dev_mode, because we need to compile From ab78b19b5f9a450a2af72a66e883401b0eb6dfd9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Jun 2021 13:31:06 +0200 Subject: [PATCH 016/333] client#115 - fixes --- .../plugins/publish/collect_texture.py | 61 +++++++++++++------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 7b79fd1061..12858595dd 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -22,6 +22,9 @@ class CollectTextures(pyblish.api.ContextPlugin): color_space = ["lin_srgb", "raw", "acesg"] + workfile_subset_template = "texturesMainWorkfile" + texture_subset_template = "texturesMain_{color_space}" + version_regex = re.compile(r"^(.+)_v([0-9]+)") udim_regex = re.compile(r"_1[0-9]{3}\.") @@ -31,7 +34,6 @@ class CollectTextures(pyblish.api.ContextPlugin): def convertor(value): return str(value) - workfile_subset = "texturesMainWorkfile" resource_files = {} workfile_files = {} representations = {} @@ -48,11 +50,19 @@ class CollectTextures(pyblish.api.ContextPlugin): for repre in instance.data["representations"]: ext = repre["ext"].replace('.', '') asset_build = version = None + + workfile_subset = self.workfile_subset_template + + if isinstance(repre["files"], list): + repre_file = repre["files"][0] + else: + repre_file = repre["files"] + if ext in self.main_workfile_extensions or \ ext in self.other_workfile_extensions: self.log.info('workfile') asset_build, version = \ - self._parse_asset_build(repre["files"], + self._parse_asset_build(repre_file, self.version_regex) asset_builds.add((asset_build, version, workfile_subset, 'workfile')) @@ -61,13 +71,9 @@ class CollectTextures(pyblish.api.ContextPlugin): if not representations.get(workfile_subset): representations[workfile_subset] = [] - # asset_build must be here to tie workfile and texture - if not workfile_files.get(asset_build): - workfile_files[asset_build] = [] - if ext in self.main_workfile_extensions: representations[workfile_subset].append(repre) - workfile_files[asset_build].append(repre["files"]) + workfile_files[asset_build] = repre_file if ext in self.other_workfile_extensions: self.log.info("other") @@ -75,8 +81,9 @@ class CollectTextures(pyblish.api.ContextPlugin): if not representations.get(workfile_subset): representations[workfile_subset].append(repre) + # only overwrite if not present if not workfile_files.get(asset_build): - workfile_files[asset_build].append(repre["files"]) + workfile_files[asset_build] = repre_file if not resource_files.get(workfile_subset): resource_files[workfile_subset] = [] @@ -88,19 +95,21 @@ class CollectTextures(pyblish.api.ContextPlugin): resource_files[workfile_subset].append(item) if ext in self.texture_extensions: - c_space = self._get_color_space(repre["files"][0], + c_space = self._get_color_space(repre_file, self.color_space) - subset = "texturesMain_{}".format(c_space) + subset_formatting_data = {"color_space": c_space} + subset = self.texture_subset_template.format( + **subset_formatting_data) asset_build, version = \ - self._parse_asset_build(repre["files"][0], + self._parse_asset_build(repre_file, self.version_regex) if not representations.get(subset): representations[subset] = [] representations[subset].append(repre) - udim = self._parse_udim(repre["files"][0], self.udim_regex) + udim = self._parse_udim(repre_file, self.udim_regex) if not version_data.get(subset): version_data[subset] = [] @@ -148,6 +157,13 @@ class CollectTextures(pyblish.api.ContextPlugin): self.log.info("-"*25) self.log.info("workfile_files:: {}".format(workfile_files)) + upd_representations = representations.get(subset) + if upd_representations and family != 'workfile': + for repre in upd_representations: + repre.pop("frameStart", None) + repre.pop("frameEnd", None) + repre.pop("fps", None) + new_instance = context.create_instance(subset) new_instance.data.update( { @@ -157,8 +173,8 @@ class CollectTextures(pyblish.api.ContextPlugin): "name": subset, "family": family, "version": int(version), - "representations": representations.get(subset), - "families": [family] + "representations": upd_representations, + "families": [] } ) if resource_files.get(subset): @@ -166,15 +182,22 @@ class CollectTextures(pyblish.api.ContextPlugin): "resources": resource_files.get(subset) }) - repre = representations.get(subset)[0] - new_instance.context.data["currentFile"] = os.path.join( - repre["stagingDir"], repre["files"][0]) + workfile = workfile_files.get(asset_build) + # store origin + if family == 'workfile': + new_instance.data["source"] = "standalone publisher" + else: + repre = representations.get(subset)[0] + new_instance.context.data["currentFile"] = os.path.join( + repre["stagingDir"], workfile) + + # add data for version document ver_data = version_data.get(subset) if ver_data: ver_data = ver_data[0] - if workfile_files.get(asset_build): - ver_data['workfile'] = workfile_files.get(asset_build)[0] + if workfile: + ver_data['workfile'] = workfile new_instance.data.update( {"versionData": ver_data} From 78a92588bb91a4a70bca912c526a15d04870f211 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Jun 2021 16:06:42 +0200 Subject: [PATCH 017/333] deadline: nuke adding settings attributes - allowed environment keys - search replace in environment values --- .../plugins/publish/submit_nuke_deadline.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 7faa3393e5..879c92490b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -32,6 +32,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): department = "" limit_groups = {} use_gpu = False + env_allowed_keys = [] + env_search_replace_values = {} def process(self, instance): instance.data["toBeRenderedOn"] = "deadline" @@ -242,18 +244,18 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "PYBLISHPLUGINPATH", "NUKE_PATH", "TOOL_ENV", - "OPENPYPE_DEV", "FOUNDRY_LICENSE" ] + # add allowed keys from preset if any + if self.env_allowed_keys: + keys += self.env_allowed_keys + environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) # self.log.debug("enviro: {}".format(pprint(environment))) + for path in os.environ: - if path.lower().startswith('pype_'): - environment[path] = os.environ[path] - if path.lower().startswith('nuke_'): - environment[path] = os.environ[path] - if 'license' in path.lower(): + if path.lower().startswith('openpype_'): environment[path] = os.environ[path] clean_environment = {} @@ -285,6 +287,13 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): environment = clean_environment # to recognize job from PYPE for turning Event On/Off environment["OPENPYPE_RENDER_JOB"] = "1" + + # finally search replace in values of any key + if self.env_search_replace_values: + for key, value in environment.items(): + for _k, _v in self.env_search_replace_values.items(): + environment[key] = value.replace(_k, _v) + payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, From 248912f9fa3b0d3611ccf898343e45e43faf4da3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Jun 2021 16:15:23 +0200 Subject: [PATCH 018/333] settings: deadline nuke submission arguments --- .../defaults/project_settings/deadline.json | 2 ++ .../projects_schema/schema_project_deadline.json | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 2cc345d5ad..5861015f2c 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -29,6 +29,8 @@ "group": "", "department": "", "use_gpu": true, + "env_allowed_keys": [], + "env_search_replace_values": {}, "limit_groups": {} }, "HarmonySubmitDeadline": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index f6a8127951..3281c9ce4d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -173,6 +173,20 @@ "key": "use_gpu", "label": "Use GPU" }, + { + "type": "list", + "key": "env_allowed_keys", + "object_type": "text", + "label": "Allowed environment keys" + }, + { + "type": "dict-modifiable", + "key": "env_search_replace_values", + "label": "Search & replace in environment values", + "object_type": { + "type": "text" + } + }, { "type": "dict-modifiable", "key": "limit_groups", From 7948820108ccfc2bd088d8125e006a3efa4d6377 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 24 Jun 2021 17:01:11 +0200 Subject: [PATCH 019/333] add shader definition item to menu --- openpype/hosts/maya/api/commands.py | 6 ++++ openpype/hosts/maya/api/menu.py | 35 +++---------------- .../defaults/project_settings/maya.json | 14 +++++--- 3 files changed, 19 insertions(+), 36 deletions(-) create mode 100644 openpype/hosts/maya/api/commands.py diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py new file mode 100644 index 0000000000..cbd8ec57f8 --- /dev/null +++ b/openpype/hosts/maya/api/commands.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +"""OpenPype script commands to be used directly in Maya.""" + +def edit_shader_definitions(): + print("Editing shader definitions...") + pass \ No newline at end of file diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 5e036b8e0c..a8812210a5 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,9 +6,11 @@ from avalon.vendor.Qt import QtWidgets, QtGui from avalon.maya import pipeline from openpype.api import BuildWorkfile import maya.cmds as cmds +from openpype.settings import get_project_settings self = sys.modules[__name__] -self._menu = os.environ.get("AVALON_LABEL") +project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) +self._menu = project_settings["maya"]["scriptsmenu"]["name"] log = logging.getLogger(__name__) @@ -55,34 +57,6 @@ def deferred(): parent=pipeline._parent ) - # Find the pipeline menu - top_menu = _get_menu(pipeline._menu) - - # Try to find workfile tool action in the menu - workfile_action = None - for action in top_menu.actions(): - if action.text() == "Work Files": - workfile_action = action - break - - # Add at the top of menu if "Work Files" action was not found - after_action = "" - if workfile_action: - # Use action's object name for `insertAfter` argument - after_action = workfile_action.objectName() - - # Insert action to menu - cmds.menuItem( - "Work Files", - parent=pipeline._menu, - command=launch_workfiles_app, - insertAfter=after_action - ) - - # Remove replaced action - if workfile_action: - top_menu.removeAction(workfile_action) - log.info("Attempting to install scripts menu ...") add_build_workfiles_item() @@ -100,8 +74,7 @@ def deferred(): return # load configuration of custom menu - config_path = os.path.join(os.path.dirname(__file__), "menu.json") - config = scriptsmenu.load_configuration(config_path) + config = project_settings["maya"]["scriptsmenu"]["definition"] # run the launcher for Maya menu studio_menu = launchformaya.main( diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 0375eb42d5..e3f0a86c27 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -10,12 +10,16 @@ "scriptsmenu": { "name": "OpenPype Tools", "definition": [ - { + { "type": "action", - "command": "$OPENPYPE_SCRIPTS\\others\\save_scene_incremental.py", - "sourcetype": "file", - "title": "# Version Up", - "tooltip": "Incremental save with a specific format" + "command": "import openpype.hosts.maya.api.commands as op_cmds; op_cmds.edit_shader_definitions()", + "sourcetype": "python", + "title": "Edit shader name definitions", + "tooltip": "Edit shader name definitions used in validation and renaming.", + "tags": [ + "pipeline", + "shader" + ] } ] }, From 8683ab3c3ff870680e711caa07698cea3ed27f8a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 24 Jun 2021 19:07:44 +0200 Subject: [PATCH 020/333] first prototype of editor --- openpype/hosts/maya/api/commands.py | 21 ++++- .../maya/api/shader_definition_editor.py | 83 +++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/maya/api/shader_definition_editor.py diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py index cbd8ec57f8..fc0dc90678 100644 --- a/openpype/hosts/maya/api/commands.py +++ b/openpype/hosts/maya/api/commands.py @@ -1,6 +1,25 @@ # -*- coding: utf-8 -*- """OpenPype script commands to be used directly in Maya.""" +import sys + def edit_shader_definitions(): + from avalon.tools import lib + from Qt import QtWidgets, QtCore + from openpype.hosts.maya.api.shader_definition_editor import ShaderDefinitionsEditor + print("Editing shader definitions...") - pass \ No newline at end of file + + module = sys.modules[__name__] + module.window = None + + top_level_widgets = QtWidgets.QApplication.topLevelWidgets() + mainwindow = next(widget for widget in top_level_widgets + if widget.objectName() == "MayaWindow") + + with lib.application(): + window = ShaderDefinitionsEditor(parent=mainwindow) + # window.setStyleSheet(style.load_stylesheet()) + window.show() + + module.window = window diff --git a/openpype/hosts/maya/api/shader_definition_editor.py b/openpype/hosts/maya/api/shader_definition_editor.py new file mode 100644 index 0000000000..88e24e6cac --- /dev/null +++ b/openpype/hosts/maya/api/shader_definition_editor.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +"""Editor for shader definitions.""" +import os +import csv +from Qt import QtWidgets, QtCore, QtGui +from openpype.lib.mongo import OpenPypeMongoConnection +from openpype import resources +import gridfs + + +class ShaderDefinitionsEditor(QtWidgets.QWidget): + + DEFINITION_FILENAME = "maya/shader_definition.csv" + + def __init__(self, parent=None): + super(ShaderDefinitionsEditor, self).__init__(parent) + self._mongo = OpenPypeMongoConnection.get_mongo_client() + self._gridfs = gridfs.GridFS( self._mongo[os.getenv("OPENPYPE_DATABASE_NAME")]) + + # TODO: handle GridIn and GridOut + self._file = self._gridfs.find_one( + {"filename": self.DEFINITION_FILENAME}) + if not self._file: + self._file = self._gridfs.new_file(filename=self.DEFINITION_FILENAME) + + self.setObjectName("shaderDefinitionEditor") + self.setWindowTitle("OpenPype shader definition editor") + icon = QtGui.QIcon(resources.pype_icon_filepath()) + self.setWindowIcon(icon) + self.setWindowFlags(QtCore.Qt.Window) + self.setParent(parent) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.resize(750, 500) + + self.setup_ui() + + def setup_ui(self): + layout = QtWidgets.QVBoxLayout(self) + self._editor = QtWidgets.QPlainTextEdit() + layout.addWidget(self._editor) + + btn_layout = QtWidgets.QHBoxLayout() + save_btn = QtWidgets.QPushButton("Save") + save_btn.clicked.connect(self._close) + + reload_btn = QtWidgets.QPushButton("Reload") + reload_btn.clicked.connect(self._reload) + + exit_btn = QtWidgets.QPushButton("Exit") + exit_btn.clicked.connect(self._close) + + btn_layout.addWidget(reload_btn) + btn_layout.addWidget(save_btn) + btn_layout.addWidget(exit_btn) + + layout.addLayout(btn_layout) + + def _read_definition_file(self): + content = [] + with open(self._file, "r") as f: + reader = csv.reader(f) + for row in reader: + content.append(row) + + return content + + def _write_definition_file(self, content): + with open(self._file, "w", newline="") as f: + writer = csv.writer(f) + writer.writerows(content.splitlines()) + + def _close(self): + self.close() + + def _reload(self): + print("reloading") + self._set_content(self._read_definition_file()) + + def _save(self): + pass + + def _set_content(self, content): + self._editor.set_content("\n".join(content)) From d889f6f24571974ac64bb4d0aa3d3aba5427ad6f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 24 Jun 2021 19:41:39 +0200 Subject: [PATCH 021/333] client#115 - added extractor to fill transfers --- .../plugins/publish/extract_resources.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py new file mode 100644 index 0000000000..1183180833 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_resources.py @@ -0,0 +1,42 @@ +import os +import pyblish.api + + +class ExtractResources(pyblish.api.InstancePlugin): + """ + Extracts files from instance.data["resources"]. + + These files are additional (textures etc.), currently not stored in + representations! + + Expects collected 'resourcesDir'. (list of dicts with 'files' key and + list of source urls) + + Provides filled 'transfers' (list of tuples (source_url, target_url)) + """ + + label = "Extract Resources SP" + hosts = ["standalonepublisher"] + order = pyblish.api.ExtractorOrder + + families = ["workfile"] + + def process(self, instance): + if not instance.data.get("resources"): + self.log.info("No resources") + return + + if not instance.data.get("transfers"): + instance.data["transfers"] = [] + + publish_dir = instance.data["resourcesDir"] + + transfers = [] + for resource in instance.data["resources"]: + for file_url in resource.get("files", []): + file_name = os.path.basename(file_url) + dest_url = os.path.join(publish_dir, file_name) + transfers.append((file_url, dest_url)) + + self.log.info("transfers:: {}".format(transfers)) + instance.data["transfers"].extend(transfers) From 14c79dca2e6bbaab2a47977ff2a287f37962fc15 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 25 Jun 2021 12:49:44 +0200 Subject: [PATCH 022/333] working editor --- .../maya/api/shader_definition_editor.py | 141 ++++++++++++++---- 1 file changed, 116 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/maya/api/shader_definition_editor.py b/openpype/hosts/maya/api/shader_definition_editor.py index 88e24e6cac..79de19069c 100644 --- a/openpype/hosts/maya/api/shader_definition_editor.py +++ b/openpype/hosts/maya/api/shader_definition_editor.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- -"""Editor for shader definitions.""" +"""Editor for shader definitions. + +Shader names are stored as simple text file over GridFS in mongodb. + +""" import os -import csv from Qt import QtWidgets, QtCore, QtGui from openpype.lib.mongo import OpenPypeMongoConnection from openpype import resources @@ -9,22 +12,21 @@ import gridfs class ShaderDefinitionsEditor(QtWidgets.QWidget): + """Widget serving as simple editor for shader name definitions.""" - DEFINITION_FILENAME = "maya/shader_definition.csv" + # name of the file used to store definitions + DEFINITION_FILENAME = "maya/shader_definition.txt" def __init__(self, parent=None): super(ShaderDefinitionsEditor, self).__init__(parent) self._mongo = OpenPypeMongoConnection.get_mongo_client() self._gridfs = gridfs.GridFS( self._mongo[os.getenv("OPENPYPE_DATABASE_NAME")]) + self._editor = None - # TODO: handle GridIn and GridOut - self._file = self._gridfs.find_one( - {"filename": self.DEFINITION_FILENAME}) - if not self._file: - self._file = self._gridfs.new_file(filename=self.DEFINITION_FILENAME) + self._original_content = self._read_definition_file() self.setObjectName("shaderDefinitionEditor") - self.setWindowTitle("OpenPype shader definition editor") + self.setWindowTitle("OpenPype shader name definition editor") icon = QtGui.QIcon(resources.pype_icon_filepath()) self.setWindowIcon(icon) self.setWindowFlags(QtCore.Qt.Window) @@ -32,16 +34,22 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.resize(750, 500) - self.setup_ui() + self._setup_ui() + self._reload() - def setup_ui(self): + def _setup_ui(self): + """Setup UI of Widget.""" layout = QtWidgets.QVBoxLayout(self) + label = QtWidgets.QLabel() + label.setText("Put shader names here - one name per line:") + layout.addWidget(label) self._editor = QtWidgets.QPlainTextEdit() + self._editor.setStyleSheet("border: none;") layout.addWidget(self._editor) btn_layout = QtWidgets.QHBoxLayout() save_btn = QtWidgets.QPushButton("Save") - save_btn.clicked.connect(self._close) + save_btn.clicked.connect(self._save) reload_btn = QtWidgets.QPushButton("Reload") reload_btn.clicked.connect(self._reload) @@ -55,29 +63,112 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): layout.addLayout(btn_layout) - def _read_definition_file(self): - content = [] - with open(self._file, "r") as f: - reader = csv.reader(f) - for row in reader: - content.append(row) + def _read_definition_file(self, file=None): + """Read definition file from database. + Args: + file (gridfs.grid_file.GridOut, Optional): File to read. If not + set, new query will be issued to find it. + + Returns: + str: Content of the file or empty string if file doesn't exist. + + """ + content = "" + if not file: + file = self._gridfs.find_one( + {"filename": self.DEFINITION_FILENAME}) + if not file: + print(">>> [SNDE]: nothing in database yet") + return content + content = file.read() + file.close() return content - def _write_definition_file(self, content): - with open(self._file, "w", newline="") as f: - writer = csv.writer(f) - writer.writerows(content.splitlines()) + def _write_definition_file(self, content, force=False): + """Write content as definition to file in database. + + Before file is writen, check is made if its content has not + changed. If is changed, warning is issued to user if he wants + it to overwrite. Note: GridFs doesn't allow changing file content. + You need to delete existing file and create new one. + + Args: + content (str): Content to write. + + Raises: + ContentException: If file is changed in database while + editor is running. + """ + file = self._gridfs.find_one( + {"filename": self.DEFINITION_FILENAME}) + if file: + content_check = self._read_definition_file(file) + if content == content_check: + print(">>> [SNDE]: content not changed") + return + if self._original_content != content_check: + if not force: + raise ContentException("Content changed") + print(">>> [SNDE]: overwriting data") + file.close() + self._gridfs.delete(file._id) + + file = self._gridfs.new_file( + filename=self.DEFINITION_FILENAME, + content_type='text/plain', + encoding='utf-8') + file.write(content) + file.close() + QtCore.QTimer.singleShot(200, self._reset_style) + self._editor.setStyleSheet("border: 1px solid #33AF65;") + self._original_content = content + + def _reset_style(self): + """Reset editor style back. + + Used to visually indicate save. + + """ + self._editor.setStyleSheet("border: none;") def _close(self): self.close() def _reload(self): - print("reloading") + print(">>> [SNDE]: reloading") self._set_content(self._read_definition_file()) def _save(self): - pass + try: + self._write_definition_file(content=self._editor.toPlainText()) + except ContentException: + # content has changed meanwhile + print(">>> [SNDE]: content has changed") + self._show_overwrite_warning() def _set_content(self, content): - self._editor.set_content("\n".join(content)) + self._editor.setPlainText(content) + + def _show_overwrite_warning(self): + reply = QtWidgets.QMessageBox.question( + self, + "Warning", + ("Content you are editing was changed meanwhile in database.\n" + "Do you want to overwrite it?"), + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + + if reply == QtWidgets.QMessageBox.Yes: + self._write_definition_file( + content=self._editor.toPlainText(), + force=True + ) + + elif reply == QtWidgets.QMessageBox.No: + # do nothing + pass + + +class ContentException(Exception): + """This is risen during save if file is changed in database.""" + pass From f5b5c944873c8159ceb88b48d4f3a6f4288e1041 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 25 Jun 2021 12:53:44 +0200 Subject: [PATCH 023/333] hound fixes --- openpype/hosts/maya/api/commands.py | 15 +++++++-------- .../hosts/maya/api/shader_definition_editor.py | 3 ++- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py index fc0dc90678..645e5840fd 100644 --- a/openpype/hosts/maya/api/commands.py +++ b/openpype/hosts/maya/api/commands.py @@ -5,21 +5,20 @@ import sys def edit_shader_definitions(): from avalon.tools import lib - from Qt import QtWidgets, QtCore - from openpype.hosts.maya.api.shader_definition_editor import ShaderDefinitionsEditor - - print("Editing shader definitions...") + from Qt import QtWidgets + from openpype.hosts.maya.api.shader_definition_editor import ( + ShaderDefinitionsEditor + ) module = sys.modules[__name__] module.window = None top_level_widgets = QtWidgets.QApplication.topLevelWidgets() - mainwindow = next(widget for widget in top_level_widgets - if widget.objectName() == "MayaWindow") + main_window = next(widget for widget in top_level_widgets + if widget.objectName() == "MayaWindow") with lib.application(): - window = ShaderDefinitionsEditor(parent=mainwindow) - # window.setStyleSheet(style.load_stylesheet()) + window = ShaderDefinitionsEditor(parent=main_window) window.show() module.window = window diff --git a/openpype/hosts/maya/api/shader_definition_editor.py b/openpype/hosts/maya/api/shader_definition_editor.py index 79de19069c..5585c9ea8e 100644 --- a/openpype/hosts/maya/api/shader_definition_editor.py +++ b/openpype/hosts/maya/api/shader_definition_editor.py @@ -20,7 +20,8 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): def __init__(self, parent=None): super(ShaderDefinitionsEditor, self).__init__(parent) self._mongo = OpenPypeMongoConnection.get_mongo_client() - self._gridfs = gridfs.GridFS( self._mongo[os.getenv("OPENPYPE_DATABASE_NAME")]) + self._gridfs = gridfs.GridFS( + self._mongo[os.getenv("OPENPYPE_DATABASE_NAME")]) self._editor = None self._original_content = self._read_definition_file() From fab1ed955313f810b26b47ba982bdf1d9c83e658 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 13:19:16 +0200 Subject: [PATCH 024/333] move environment filling before mac specific check --- openpype/lib/applications.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index a7dcb6dd55..c8380dd3e0 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -449,6 +449,12 @@ class ApplicationExecutable: """Representation of executable loaded from settings.""" def __init__(self, executable): + # Try to format executable with environments + try: + executable = executable.format(**os.environ) + except Exception: + pass + # On MacOS check if exists path to executable when ends with `.app` # - it is common that path will lead to "/Applications/Blender" but # real path is "/Applications/Blender.app" @@ -460,12 +466,6 @@ class ApplicationExecutable: if os.path.exists(_executable): executable = _executable - # Try to format executable with environments - try: - executable = executable.format(**os.environ) - except Exception: - pass - self.executable_path = executable def __str__(self): From 5254a53d035d32d049a9b2c40f2887feae6dc5e6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 25 Jun 2021 19:15:53 +0200 Subject: [PATCH 025/333] validator --- .../plugins/publish/validate_model_name.py | 54 +++++++++++++------ .../defaults/project_settings/maya.json | 3 +- .../schemas/schema_maya_publish.json | 7 ++- 3 files changed, 46 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index 98da4d42ba..d031a8b76c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -1,8 +1,13 @@ +# -*- coding: utf-8 -*- +"""Validate model nodes names.""" from maya import cmds import pyblish.api import openpype.api import openpype.hosts.maya.api.action +from openpype.lib.mongo import OpenPypeMongoConnection +import gridfs import re +import os class ValidateModelName(pyblish.api.InstancePlugin): @@ -19,18 +24,18 @@ class ValidateModelName(pyblish.api.InstancePlugin): families = ["model"] label = "Model Name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] - # path to shader names definitions - # TODO: move it to preset file material_file = None - regex = '(.*)_(\\d)*_(.*)_(GEO)' + database_file = "maya/shader_definition.txt" @classmethod def get_invalid(cls, instance): + """Get invalid nodes.""" + use_db = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["database"] # noqa: E401 - # find out if supplied transform is group or not - def is_group(groupName): + def is_group(group_name): + """Find out if supplied transform is group or not.""" try: - children = cmds.listRelatives(groupName, children=True) + children = cmds.listRelatives(group_name, children=True) for child in children: if not cmds.ls(child, transforms=True): return False @@ -49,24 +54,41 @@ class ValidateModelName(pyblish.api.InstancePlugin): fullPath=True) or [] descendants = cmds.ls(descendants, noIntermediate=True, long=True) - trns = cmds.ls(descendants, long=False, type=('transform')) + trns = cmds.ls(descendants, long=False, type='transform') # filter out groups - filter = [node for node in trns if not is_group(node)] + filtered = [node for node in trns if not is_group(node)] # load shader list file as utf-8 - if cls.material_file: - shader_file = open(cls.material_file, "r") - shaders = shader_file.readlines() + shaders = [] + if not use_db: + if cls.material_file: + if os.path.isfile(cls.material_file): + shader_file = open(cls.material_file, "r") + shaders = shader_file.readlines() + shader_file.close() + else: + cls.log.error("Missing shader name definition file.") + return True + else: + client = OpenPypeMongoConnection.get_mongo_client() + fs = gridfs.GridFS(client[os.getenv("OPENPYPE_DATABASE_NAME")]) + shader_file = fs.find_one({"filename": cls.database_file}) + if not shader_file: + cls.log.error("Missing shader name definition in database.") + return True + shaders = shader_file.read().splitlines() shader_file.close() # strip line endings from list shaders = map(lambda s: s.rstrip(), shaders) # compile regex for testing names - r = re.compile(cls.regex) + regex = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["regex"] # noqa: E401 + r = re.compile(regex) - for obj in filter: + for obj in filtered: + cls.log.info("testing: {}".format(obj)) m = r.match(obj) if m is None: cls.log.error("invalid name on: {}".format(obj)) @@ -74,7 +96,7 @@ class ValidateModelName(pyblish.api.InstancePlugin): else: # if we have shader files and shader named group is in # regex, test this group against names in shader file - if 'shader' in r.groupindex and shaders: + if "shader" in r.groupindex and shaders: try: if not m.group('shader') in shaders: cls.log.error( @@ -90,8 +112,8 @@ class ValidateModelName(pyblish.api.InstancePlugin): return invalid def process(self, instance): - + """Plugin entry point.""" invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Model naming is invalid. See log.") + raise RuntimeError("Model naming is invalid. See the log.") diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e3f0a86c27..b40ab40c61 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -164,12 +164,13 @@ }, "ValidateModelName": { "enabled": false, + "database": true, "material_file": { "windows": "", "darwin": "", "linux": "" }, - "regex": "(.*)_(\\\\d)*_(.*)_(GEO)" + "regex": "(.*)_(\\d)*_(?P.*)_(GEO)" }, "ValidateTransformNamingSuffix": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 5ca7059ee5..10b80dddfd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -147,9 +147,14 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "boolean", + "key": "database", + "label": "Use database shader name definitions" + }, { "type": "label", - "label": "Path to material file defining list of material names to check. This is material name per line simple text file.
It will be checked against named group shader in your Validation regex.

For example:
^.*(?P=<shader>.+)_GEO

" + "label": "Path to material file defining list of material names to check. This is material name per line simple text file.
It will be checked against named group shader in your Validation regex.

For example:
^.*(?P=<shader>.+)_GEO

This is used instead of database definitions if they are disabled." }, { "type": "path", From 325835c860826a2e9ab1330cffe4f27d35062a37 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 25 Jun 2021 19:18:35 +0200 Subject: [PATCH 026/333] fix hound --- openpype/hosts/maya/plugins/publish/validate_model_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index d031a8b76c..84242cda23 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -30,7 +30,7 @@ class ValidateModelName(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): """Get invalid nodes.""" - use_db = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["database"] # noqa: E401 + use_db = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["database"] # noqa: E501 def is_group(group_name): """Find out if supplied transform is group or not.""" @@ -84,7 +84,7 @@ class ValidateModelName(pyblish.api.InstancePlugin): shaders = map(lambda s: s.rstrip(), shaders) # compile regex for testing names - regex = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["regex"] # noqa: E401 + regex = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["regex"] # noqa: E501 r = re.compile(regex) for obj in filtered: From c69b20930c92281bc5b6bc2d4e821855f6612c0b Mon Sep 17 00:00:00 2001 From: Derek Severin Date: Sun, 27 Jun 2021 17:31:57 +0700 Subject: [PATCH 027/333] Minor doc fixes --- website/docs/dev_build.md | 4 ++-- website/docs/module_ftrack.md | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/website/docs/dev_build.md b/website/docs/dev_build.md index 2c4bd1e9af..b3e0c24fc2 100644 --- a/website/docs/dev_build.md +++ b/website/docs/dev_build.md @@ -137,12 +137,12 @@ $ pyenv install -v 3.7.10 $ cd /path/to/pype-3 # set local python version -$ pyenv local 3.7.9 +$ pyenv local 3.7.10 ``` :::note Install build requirements for **Ubuntu** ```shell -sudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git +sudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git patchelf ``` In case you run in error about `xcb` when running Pype, diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index bd0dbaef4f..9911dee45a 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -34,7 +34,7 @@ To prepare Ftrack for working with OpenPype you'll need to run [OpenPype Admin - Ftrack Event Server is the key to automation of many tasks like _status change_, _thumbnail update_, _automatic synchronization to Avalon database_ and many more. Event server should run at all times to perform the required processing as it is not possible to catch some of them retrospectively with enough certainty. ### Running event server -There are specific launch arguments for event server. With `openpype eventserver` you can launch event server but without prior preparation it will terminate immediately. The reason is that event server requires 3 pieces of information: _Ftrack server url_, _paths to events_ and _credentials (Username and API key)_. Ftrack server URL and Event path are set from OpenPype's environments by default, but the credentials must be done separatelly for security reasons. +There are specific launch arguments for event server. With `openpype_console eventserver` you can launch event server but without prior preparation it will terminate immediately. The reason is that event server requires 3 pieces of information: _Ftrack server url_, _paths to events_ and _credentials (Username and API key)_. Ftrack server URL and Event path are set from OpenPype's environments by default, but the credentials must be done separatelly for security reasons. @@ -56,7 +56,7 @@ There are specific launch arguments for event server. With `openpype eventserver - `--ftrack-url "https://yourdomain.ftrackapp.com/"` : Ftrack server URL _(it is not needed to enter if you have set `FTRACK_SERVER` in OpenPype' environments)_ - `--ftrack-events-path "//Paths/To/Events/"` : Paths to events folder. May contain multiple paths separated by `;`. _(it is not needed to enter if you have set `FTRACK_EVENTS_PATH` in OpenPype' environments)_ -So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype.exe eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype.exe eventserver`. +So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype_console.exe eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype_console.exe eventserver`. @@ -100,14 +100,17 @@ Event server should **not** run more than once! It may cause major issues. - create file: - `sudo vi /opt/OpenPype/run_event_server.sh` + `sudo vi /opt/openpype/run_event_server.sh` - add content to the file: ```sh -#!\usr\bin\env +#!/usr/bin/env export OPENPYPE_DEBUG=3 pushd /mnt/pipeline/prod/openpype-setup -. openpype eventserver --ftrack-user --ftrack-api-key +. openpype_console eventserver --ftrack-user --ftrack-api-key ``` +- change file permission: + `sudo chmod 0755 /opt/openpype/run_event_server.sh` + - create service file: `sudo vi /etc/systemd/system/openpype-ftrack-event-server.service` - add content to the service file @@ -145,7 +148,7 @@ WantedBy=multi-user.target @echo off set OPENPYPE_DEBUG=3 pushd \\path\to\file\ -call openpype.bat eventserver --ftrack-user --ftrack-api-key +call openpype_console.exe eventserver --ftrack-user --ftrack-api-key ``` - download and install `nssm.cc` - create Windows service according to nssm.cc manual From c1d147934e8af6a600e8748f99b88a51d677d4b1 Mon Sep 17 00:00:00 2001 From: Derek Severin Date: Sun, 27 Jun 2021 17:55:16 +0700 Subject: [PATCH 028/333] Removed 'call' --- website/docs/module_ftrack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index 9911dee45a..6d56277c67 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -148,7 +148,7 @@ WantedBy=multi-user.target @echo off set OPENPYPE_DEBUG=3 pushd \\path\to\file\ -call openpype_console.exe eventserver --ftrack-user --ftrack-api-key +openpype_console.exe eventserver --ftrack-user --ftrack-api-key ``` - download and install `nssm.cc` - create Windows service according to nssm.cc manual From 1c54a4c23e539b417015089f02f9a60af08e07cc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 28 Jun 2021 09:20:44 +0200 Subject: [PATCH 029/333] client#115 - added udim support to integrate_new Fixes --- .../plugins/publish/collect_texture.py | 200 ++++++++++++++---- .../publish/extract_workfile_location.py | 41 ++++ openpype/plugins/publish/integrate_new.py | 24 ++- .../defaults/project_anatomy/templates.json | 2 +- 4 files changed, 216 insertions(+), 51 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 12858595dd..0e2b21927f 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -1,13 +1,20 @@ import os -import copy import re -import opentimelineio as otio import pyblish.api -from openpype import lib as plib import json +from avalon.api import format_template_with_optional_keys + + class CollectTextures(pyblish.api.ContextPlugin): - """Collect workfile (and its resource_files) and textures.""" + """Collect workfile (and its resource_files) and textures. + + Provides: + 1 instance per workfile (with 'resources' filled if needed) + (workfile family) + 1 instance per group of textures + (textures family) + """ order = pyblish.api.CollectorOrder label = "Collect Textures" @@ -22,14 +29,29 @@ class CollectTextures(pyblish.api.ContextPlugin): color_space = ["lin_srgb", "raw", "acesg"] - workfile_subset_template = "texturesMainWorkfile" - texture_subset_template = "texturesMain_{color_space}" + version_regex = re.compile(r"v([0-9]+)") + udim_regex = re.compile(r"_1[0-9]{3}\.") + + #currently implemented placeholders ["color_space"] + input_naming_patterns = { + # workfile: ctr_envCorridorMain_texturing_v005.mra > + # expected groups: [(asset),(filler),(version)] + # texture: T_corridorMain_aluminium1_BaseColor_lin_srgb_1029.exr + # expected groups: [(asset), (filler),(color_space),(udim)] + r'^ctr_env([^.]+)_(.+)_v([0-9]{3,}).+': + r'^T_([^_.]+)_(.*)_({color_space})_(1[0-9]{3}).+' + } + + workfile_subset_template = "textures{}Workfile" + # implemented keys: ["color_space", "channel", "subset"] + texture_subset_template = "textures{subset}_{channel}" version_regex = re.compile(r"^(.+)_v([0-9]+)") udim_regex = re.compile(r"_1[0-9]{3}\.") def process(self, context): self.context = context + import json def convertor(value): return str(value) @@ -41,9 +63,19 @@ class CollectTextures(pyblish.api.ContextPlugin): asset_builds = set() asset = None for instance in context: + if not self.input_naming_patterns: + raise ValueError("Naming patterns are not configured. \n" + "Ask admin to provide naming conventions " + "for workfiles and textures.") + if not asset: asset = instance.data["asset"] # selected from SP + parsed_subset = instance.data["subset"].replace( + instance.data["family"], '') + workfile_subset = self.workfile_subset_template.format( + parsed_subset) + self.log.info("instance.data:: {}".format( json.dumps(instance.data, indent=4, default=convertor))) processed_instance = False @@ -51,19 +83,20 @@ class CollectTextures(pyblish.api.ContextPlugin): ext = repre["ext"].replace('.', '') asset_build = version = None - workfile_subset = self.workfile_subset_template - if isinstance(repre["files"], list): repre_file = repre["files"][0] else: repre_file = repre["files"] if ext in self.main_workfile_extensions or \ - ext in self.other_workfile_extensions: - self.log.info('workfile') - asset_build, version = \ - self._parse_asset_build(repre_file, - self.version_regex) + ext in self.other_workfile_extensions: + + asset_build = self._get_asset_build( + repre_file, + self.input_naming_patterns.keys(), + self.color_space + ) + version = self._get_version(repre_file, self.version_regex) asset_builds.add((asset_build, version, workfile_subset, 'workfile')) processed_instance = True @@ -95,15 +128,32 @@ class CollectTextures(pyblish.api.ContextPlugin): resource_files[workfile_subset].append(item) if ext in self.texture_extensions: - c_space = self._get_color_space(repre_file, - self.color_space) - subset_formatting_data = {"color_space": c_space} - subset = self.texture_subset_template.format( - **subset_formatting_data) + c_space = self._get_color_space( + repre_file, + self.color_space + ) - asset_build, version = \ - self._parse_asset_build(repre_file, - self.version_regex) + channel = self._get_channel_name( + repre_file, + list(self.input_naming_patterns.values()), + self.color_space + ) + + formatting_data = { + "color_space": c_space, + "channel": channel, + "subset": parsed_subset + } + self.log.debug("data::{}".format(formatting_data)) + subset = format_template_with_optional_keys( + formatting_data, self.texture_subset_template) + + asset_build = self._get_asset_build( + repre_file, + self.input_naming_patterns.values(), + self.color_space + ) + version = self._get_version(repre_file, self.version_regex) if not representations.get(subset): representations[subset] = [] @@ -149,21 +199,15 @@ class CollectTextures(pyblish.api.ContextPlugin): representations (dict) of representation files, key is asset_build """ + # sort workfile first + asset_builds = sorted(asset_builds, + key=lambda tup: tup[3], reverse=True) + + # workfile must have version, textures might + main_version = None for asset_build, version, subset, family in asset_builds: - - self.log.info("resources:: {}".format(resource_files)) - self.log.info("-"*25) - self.log.info("representations:: {}".format(representations)) - self.log.info("-"*25) - self.log.info("workfile_files:: {}".format(workfile_files)) - - upd_representations = representations.get(subset) - if upd_representations and family != 'workfile': - for repre in upd_representations: - repre.pop("frameStart", None) - repre.pop("frameEnd", None) - repre.pop("fps", None) - + if not main_version: + main_version = version new_instance = context.create_instance(subset) new_instance.data.update( { @@ -172,8 +216,7 @@ class CollectTextures(pyblish.api.ContextPlugin): "label": subset, "name": subset, "family": family, - "version": int(version), - "representations": upd_representations, + "version": int(version or main_version), "families": [] } ) @@ -203,18 +246,43 @@ class CollectTextures(pyblish.api.ContextPlugin): {"versionData": ver_data} ) - self.log.info("new instance:: {}".format(json.dumps(new_instance.data, indent=4))) + upd_representations = representations.get(subset) + if upd_representations and family != 'workfile': + upd_representations = self._update_representations( + upd_representations) - def _parse_asset_build(self, name, version_regex): - regex_result = version_regex.findall(name) - asset_name = None # ?? - version_number = 1 - if regex_result: - asset_name, version_number = regex_result[0] + new_instance.data["representations"] = upd_representations - return asset_name, version_number + def _get_asset_build(self, name, input_naming_patterns, color_spaces): + """Loops through configured workfile patterns to find asset name. - def _parse_udim(self, name, udim_regex): + Asset name used to bind workfile and its textures. + + Args: + name (str): workfile name + input_naming_patterns (list): + [workfile_pattern] or [texture_pattern] + """ + for input_pattern in input_naming_patterns: + for cs in color_spaces: + pattern = input_pattern.replace('{color_space}', cs) + regex_result = re.findall(pattern, name) + + if regex_result: + asset_name = regex_result[0][0].lower() + return asset_name + + raise ValueError("Couldnt find asset name in {}".format(name)) + + def _get_version(self, name, version_regex): + found = re.search(version_regex, name) + if found: + return found.group().replace("v", "") + + self.log.info("No version found in the name {}".format(name)) + + def _get_udim(self, name, udim_regex): + """Parses from 'name' udim value with 'udim_regex'.""" regex_result = udim_regex.findall(name) udim = None if not regex_result: @@ -225,7 +293,11 @@ class CollectTextures(pyblish.api.ContextPlugin): return udim def _get_color_space(self, name, color_spaces): - """Looks for color_space from a list in a file name.""" + """Looks for color_space from a list in a file name. + + Color space seems not to be recognizable by regex pattern, set of + known space spaces must be provided. + """ color_space = None found = [cs for cs in color_spaces if re.search("_{}_".format(cs), name)] @@ -241,3 +313,37 @@ class CollectTextures(pyblish.api.ContextPlugin): color_space = found[0] return color_space + + def _get_channel_name(self, name, input_naming_patterns, color_spaces): + """Return parsed channel name. + + Unknown format of channel name and color spaces >> cs are known + list - 'color_space' used as a placeholder + """ + for texture_pattern in input_naming_patterns: + for cs in color_spaces: + pattern = texture_pattern.replace('{color_space}', cs) + ret = re.findall(pattern, name) + if ret: + return ret.pop()[1] + + def _update_representations(self, upd_representations): + """Frames dont have sense for textures, add collected udims instead.""" + udims = [] + for repre in upd_representations: + repre.pop("frameStart", None) + repre.pop("frameEnd", None) + repre.pop("fps", None) + + files = repre.get("files", []) + if not isinstance(files, list): + files = [files] + + for file_name in files: + udim = self._get_udim(file_name, self.udim_regex) + udims.append(udim) + + repre["udim"] = udims # must be this way, used for filling path + + return upd_representations + diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py new file mode 100644 index 0000000000..4345cef6dc --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py @@ -0,0 +1,41 @@ +import os +import pyblish.api + + +class ExtractWorkfileUrl(pyblish.api.ContextPlugin): + """ + Modifies 'workfile' field to contain link to published workfile. + + Expects that batch contains only single workfile and matching + (multiple) textures. + """ + + label = "Extract Workfile Url SP" + hosts = ["standalonepublisher"] + order = pyblish.api.ExtractorOrder + + families = ["textures"] + + def process(self, context): + filepath = None + + # first loop for workfile + for instance in context: + if instance.data["family"] == 'workfile': + anatomy = context.data['anatomy'] + template_data = instance.data.get("anatomyData") + rep_name = instance.data.get("representations")[0].get("name") + template_data["representation"] = rep_name + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled["publish"]["path"] + filepath = os.path.normpath(template_filled) + self.log.info("Using published scene for render {}".format( + filepath)) + + if not filepath: + raise ValueError("Texture batch doesn't contain workfile.") + + # then apply to all textures + for instance in context: + if instance.data["family"] == 'textures': + instance.data["versionData"]["workfile"] = filepath diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index c5ce6d23aa..6d2a95f232 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -380,7 +380,12 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): test_dest_files = list() for i in [1, 2]: - template_data["frame"] = src_padding_exp % i + template_data["representation"] = repre['ext'] + if not repre.get("udim"): + template_data["frame"] = src_padding_exp % i + else: + template_data["udim"] = src_padding_exp % i + anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled[template_name]["path"] if repre_context is None: @@ -388,7 +393,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): test_dest_files.append( os.path.normpath(template_filled) ) - template_data["frame"] = repre_context["frame"] + if not repre.get("udim"): + template_data["frame"] = repre_context["frame"] + else: + template_data["udim"] = repre_context["udim"] self.log.debug( "test_dest_files: {}".format(str(test_dest_files))) @@ -453,7 +461,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst_start_frame = dst_padding # Store used frame value to template data - template_data["frame"] = dst_start_frame + if repre.get("frame"): + template_data["frame"] = dst_start_frame + dst = "{0}{1}{2}".format( dst_head, dst_start_frame, @@ -476,6 +486,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "Given file name is a full path" ) + template_data["representation"] = repre['ext'] + # Store used frame value to template data + if repre.get("udim"): + template_data["udim"] = repre["udim"][0] src = os.path.join(stagingdir, fname) anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled[template_name]["path"] @@ -488,6 +502,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): repre['published_path'] = dst self.log.debug("__ dst: {}".format(dst)) + if repre.get("udim"): + repre_context["udim"] = repre.get("udim") # store list + repre["publishedFiles"] = published_files for key in self.db_representation_context_keys: @@ -1045,6 +1062,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ) ) shutil.copy(file_url, new_name) + os.remove(file_url) else: self.log.debug( "Renaming file {} to {}".format( diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 63477b9d82..53abd35ed5 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -17,7 +17,7 @@ }, "publish": { "folder": "{root[work]}/{project[name]}/{hierarchy}/{asset}/publish/{family}/{subset}/{@version}", - "file": "{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}>.{ext}", + "file": "{project[code]}_{asset}_{subset}_{@version}<_{output}><.{@frame}><_{udim}>.{ext}", "path": "{@folder}/{@file}", "thumbnail": "{thumbnail_root}/{project[name]}/{_id}_{thumbnail_type}.{ext}" }, From c98aafea8ef5f18bd31bdd53c13c52a299395b9b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:35:34 +0200 Subject: [PATCH 030/333] base of dict contitional --- .../settings/entities/dict_conditional.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 openpype/settings/entities/dict_conditional.py diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py new file mode 100644 index 0000000000..01da57a190 --- /dev/null +++ b/openpype/settings/entities/dict_conditional.py @@ -0,0 +1,33 @@ +import copy +import collections + +from .lib import ( + WRAPPER_TYPES, + OverrideState, + NOT_SET +) +from openpype.settings.constants import ( + METADATA_KEYS, + M_OVERRIDEN_KEY, + KEY_REGEX +) +from . import ( + BaseItemEntity, + ItemEntity, + BoolEntity, + GUIEntity +) +from .exceptions import ( + SchemaDuplicatedKeys, + EntitySchemaError, + InvalidKeySymbols +) + + +class DictConditionalEntity(ItemEntity): + schema_types = ["dict-conditional"] + _default_label_wrap = { + "use_label_wrap": False, + "collapsible": False, + "collapsed": True + } From 6c63bc048fe2e12f82581a14797dc8776d65f46e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:35:50 +0200 Subject: [PATCH 031/333] added example schema for reference --- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 01da57a190..8e7a7b79c9 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -24,6 +24,52 @@ from .exceptions import ( ) +example_schema = { + "type": "dict-conditional", + "key": "KEY", + "label": "LABEL", + "enum_key": "type", + "enum_label": "label", + "enum_children": [ + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "command", + "label": "Comand" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "type": "list", + "object_type": "text" + } + ] + }, + { + "key": "separator", + "label": "Separator" + } + ] +} + + class DictConditionalEntity(ItemEntity): schema_types = ["dict-conditional"] _default_label_wrap = { From 1063f8210ab0ec1c8e7c41493408935023f88cda Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:36:38 +0200 Subject: [PATCH 032/333] implemented `_item_initalization` similar to 'dict' entity --- .../settings/entities/dict_conditional.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8e7a7b79c9..989d69a290 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -77,3 +77,33 @@ class DictConditionalEntity(ItemEntity): "collapsible": False, "collapsed": True } + + def _item_initalization(self): + self._default_metadata = NOT_SET + self._studio_override_metadata = NOT_SET + self._project_override_metadata = NOT_SET + + self._ignore_child_changes = False + + # `current_metadata` are still when schema is loaded + # - only metadata stored with dict item are gorup overrides in + # M_OVERRIDEN_KEY + self._current_metadata = {} + self._metadata_are_modified = False + + # Children are stored by key as keys are immutable and are defined by + # schema + self.valid_value_types = (dict, ) + self.children = collections.defaultdict(list) + self.non_gui_children = collections.defaultdict(dict) + self.gui_layout = collections.defaultdict(list) + + if self.is_dynamic_item: + self.require_key = False + + self.enum_key = self.schema_data.get("enum_key") + self.enum_label = self.schema_data.get("enum_label") + self.enum_children = self.schema_data.get("enum_children") + + self.enum_entity = None + self.current_enum = None From 82f1817ec0ca4fca3cd6efca01b28e28c76b1715 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:39:32 +0200 Subject: [PATCH 033/333] implemented _add_children method --- .../settings/entities/dict_conditional.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 989d69a290..da6df6170d 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -107,3 +107,61 @@ class DictConditionalEntity(ItemEntity): self.enum_entity = None self.current_enum = None + + self._add_children() + + def _add_children(self): + """Add children from schema data and repare enum items. + + Each enum item must have defined it's children. None are shared across + all enum items. + + Nice to have: Have ability to have shared keys across all enum items. + + All children are stored by their enum item. + """ + # Skip and wait for validation + if not self.enum_children or not self.enum_key: + return + + enum_items = [] + valid_enum_items = [] + for item in self.enum_children: + if isinstance(item, dict) and "key" in item: + valid_enum_items.append(item) + + first_key = None + for item in valid_enum_items: + item_key = item["key"] + if first_key is None: + first_key = item_key + item_label = item.get("label") or item_key + enum_items.append({item_key: item_label}) + + if not enum_items: + return + + self.current_enum = first_key + + enum_key = self.enum_key or "invalid" + enum_schema = { + "type": "enum", + "multiselection": False, + "enum_items": enum_items, + "key": enum_key, + "label": self.enum_label or enum_key + } + enum_entity = self.create_schema_object(enum_schema, self) + self.enum_entity = enum_entity + + for item in valid_enum_items: + item_key = item["key"] + children = item.get("children") or [] + for children_schema in children: + child_obj = self.create_schema_object(children_schema, self) + self.children[item_key].append(child_obj) + self.gui_layout[item_key].append(child_obj) + if isinstance(child_obj, GUIEntity): + continue + + self.non_gui_children[item_key][child_obj.key] = child_obj From 694f6b58cb0467aa7ddd496a60a61ef220db31b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:39:54 +0200 Subject: [PATCH 034/333] added schema validations of conditional dictionary --- .../settings/entities/dict_conditional.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index da6df6170d..20ee80337d 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -110,6 +110,67 @@ class DictConditionalEntity(ItemEntity): self._add_children() + def schema_validations(self): + """Validation of schema data.""" + if self.enum_key is None: + raise EntitySchemaError(self, "Key 'enum_key' is not set.") + + if not isinstance(self.enum_children, list): + raise EntitySchemaError( + self, "Key 'enum_children' must be a list. Got: {}".format( + str(type(self.enum_children)) + ) + ) + + if not self.enum_children: + raise EntitySchemaError(self, ( + "Key 'enum_children' have empty value. Entity can't work" + " without children definitions." + )) + + children_def_keys = [] + for children_def in self.enum_children: + if not isinstance(children_def, dict): + raise EntitySchemaError(( + "Children definition under key 'enum_children' must" + " be a dictionary." + )) + + if "key" not in children_def: + raise EntitySchemaError(( + "Children definition under key 'enum_children' miss" + " 'key' definition." + )) + # We don't validate regex of these keys because they will be stored + # as value at the end. + key = children_def["key"] + if key in children_def_keys: + # TODO this hould probably be different exception? + raise SchemaDuplicatedKeys(self, key) + children_def_keys.append(key) + + for children in self.children.values(): + children_keys = set() + children_keys.add(self.enum_key) + for child_entity in children: + if not isinstance(child_entity, BaseItemEntity): + continue + elif child_entity.key not in children_keys: + children_keys.add(child_entity.key) + else: + raise SchemaDuplicatedKeys(self, child_entity.key) + + for children_by_key in self.non_gui_children.values(): + for key in children_by_key.keys(): + if not KEY_REGEX.match(key): + raise InvalidKeySymbols(self.path, key) + + super(DictConditionalEntity, self).schema_validations() + # Trigger schema validation on children entities + for children in self.children.values(): + for child_obj in children: + child_obj.schema_validations() + def _add_children(self): """Add children from schema data and repare enum items. From ff701860f704c186fe969d76042747bd227bb331 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:48:09 +0200 Subject: [PATCH 035/333] implemented `get_child_path` --- .../settings/entities/dict_conditional.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 20ee80337d..79b5624505 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -226,3 +226,24 @@ class DictConditionalEntity(ItemEntity): continue self.non_gui_children[item_key][child_obj.key] = child_obj + + def get_child_path(self, child_obj): + """Get hierarchical path of child entity. + + Child must be entity's direct children. This must be possible to get + for any children even if not from current enum value. + """ + if child_obj is self.enum_entity: + return "/".join([self.path, self.enum_key]) + + result_key = None + for children in self.non_gui_children.values(): + for key, _child_obj in children.items(): + if _child_obj is child_obj: + result_key = key + break + + if result_key is None: + raise ValueError("Didn't found child {}".format(child_obj)) + + return "/".join([self.path, result_key]) From 218158ddc4961e24788c332fb74104678452ed4d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:51:56 +0200 Subject: [PATCH 036/333] implemented base dictionary methods --- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 79b5624505..71e727e53f 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -78,6 +78,52 @@ class DictConditionalEntity(ItemEntity): "collapsed": True } + def __getitem__(self, key): + """Return entity inder key.""" + return self.non_gui_children[self.current_enum][key] + + def __setitem__(self, key, value): + """Set value of item under key.""" + child_obj = self.non_gui_children[self.current_enum][key] + child_obj.set(value) + + def __iter__(self): + """Iter through keys.""" + for key in self.keys(): + yield key + + def __contains__(self, key): + """Check if key is available.""" + return key in self.non_gui_children[self.current_enum] + + def get(self, key, default=None): + """Safe entity getter by key.""" + return self.non_gui_children[self.current_enum].get(key, default) + + def keys(self): + """Entity's keys.""" + keys = list(self.non_gui_children[self.current_enum].keys()) + keys.insert(0, [self.current_enum]) + return keys + + def values(self): + """Children entities.""" + values = [ + self.enum_entity + ] + for child_entiy in self.non_gui_children[self.current_enum].values(): + values.append(child_entiy) + return values + + def items(self): + """Children entities paired with their key (key, value).""" + items = [ + (self.enum_key, self.enum_entity) + ] + for key, value in self.non_gui_children[self.current_enum].items(): + items.append((key, value)) + return items + def _item_initalization(self): self._default_metadata = NOT_SET self._studio_override_metadata = NOT_SET From 9ccc667c85471437c5acace23f2279b13e97380a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:53:45 +0200 Subject: [PATCH 037/333] implemented idea of set value --- openpype/settings/entities/dict_conditional.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 71e727e53f..a933dfd586 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -124,6 +124,16 @@ class DictConditionalEntity(ItemEntity): items.append((key, value)) return items + def set(self, value): + """Set value.""" + new_value = self.convert_to_valid_type(value) + # First change value of enum key if available + if self.enum_key in new_value: + self.enum_entity.set(new_value.pop(self.enum_key)) + + for _key, _value in new_value.items(): + self.non_gui_children[self.current_enum][_key].set(_value) + def _item_initalization(self): self._default_metadata = NOT_SET self._studio_override_metadata = NOT_SET From 469dcc09fa7051313039b81e7ff4a6ebcdf840aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:55:34 +0200 Subject: [PATCH 038/333] implemented change callbacks --- .../settings/entities/dict_conditional.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index a933dfd586..9e6b5b6f36 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -227,6 +227,25 @@ class DictConditionalEntity(ItemEntity): for child_obj in children: child_obj.schema_validations() + def on_change(self): + """Update metadata on change and pass change to parent.""" + self._update_current_metadata() + + for callback in self.on_change_callbacks: + callback() + self.parent.on_child_change(self) + + def on_child_change(self, child_obj): + """Trigger on change callback if child changes are not ignored.""" + if self._ignore_child_changes: + return + + if ( + child_obj is self.enum_entity + or child_obj in self.children[self.current_enum] + ): + self.on_change() + def _add_children(self): """Add children from schema data and repare enum items. From be1f0f77a05351bd7c3a154ad35e787a392701d3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:58:55 +0200 Subject: [PATCH 039/333] implemented set_override_state --- openpype/settings/entities/dict_conditional.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 9e6b5b6f36..8bf9b87218 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -322,3 +322,20 @@ class DictConditionalEntity(ItemEntity): raise ValueError("Didn't found child {}".format(child_obj)) return "/".join([self.path, result_key]) + + def set_override_state(self, state): + # Trigger override state change of root if is not same + if self.root_item.override_state is not state: + self.root_item.set_override_state(state) + return + + # Change has/had override states + self._override_state = state + + self.enum_entity.set_override_state(state) + + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.set_override_state(state) + + self._update_current_metadata() From 92d0c9f37b3e116700ff25a3422acfb3436aefe6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:02:05 +0200 Subject: [PATCH 040/333] implemented `value` and `settings_value` --- .../settings/entities/dict_conditional.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8bf9b87218..d2bab1ed15 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -339,3 +339,53 @@ class DictConditionalEntity(ItemEntity): child_obj.set_override_state(state) self._update_current_metadata() + + @property + def value(self): + output = { + self.current_enum: self.enum_entity.value + } + for key, child_obj in self.non_gui_children[self.current_enum].items(): + output[key] = child_obj.value + return output + + def settings_value(self): + if self._override_state is OverrideState.NOT_DEFINED: + return NOT_SET + + if self._override_state is OverrideState.DEFAULTS: + output = { + self.current_enum: self.enum_entity.settings_value() + } + non_gui_children = self.non_gui_children[self.current_enum] + for key, child_obj in non_gui_children.items(): + child_value = child_obj.settings_value() + if not child_obj.is_file and not child_obj.file_item: + for _key, _value in child_value.items(): + new_key = "/".join([key, _key]) + output[new_key] = _value + else: + output[key] = child_value + return output + + if self.is_group: + if self._override_state is OverrideState.STUDIO: + if not self.has_studio_override: + return NOT_SET + elif self._override_state is OverrideState.PROJECT: + if not self.has_project_override: + return NOT_SET + + output = { + self.current_enum: self.enum_entity.settings_value() + } + for key, child_obj in self.non_gui_children[self.current_enum].items(): + value = child_obj.settings_value() + if value is not NOT_SET: + output[key] = value + + if not output: + return NOT_SET + + output.update(self._current_metadata) + return output From 04207c689fe584c8c2393ad95893f67d6e5dc4b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:02:23 +0200 Subject: [PATCH 041/333] implemented modification and override properties --- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d2bab1ed15..f82cc02e3e 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -349,6 +349,52 @@ class DictConditionalEntity(ItemEntity): output[key] = child_obj.value return output + @property + def has_unsaved_changes(self): + if self._metadata_are_modified: + return True + + return self._child_has_unsaved_changes + + @property + def _child_has_unsaved_changes(self): + if self.enum_entity.has_unsaved_changes: + return True + for child_obj in self.non_gui_children[self.current_enum].values(): + if child_obj.has_unsaved_changes: + return True + return False + + @property + def has_studio_override(self): + return self._child_has_studio_override + + @property + def _child_has_studio_override(self): + if self._override_state >= OverrideState.STUDIO: + if self.enum_entity.has_studio_override: + return True + + for child_obj in self.non_gui_children[self.current_enum].values(): + if child_obj.has_studio_override: + return True + return False + + @property + def has_project_override(self): + return self._child_has_project_override + + @property + def _child_has_project_override(self): + if self._override_state >= OverrideState.PROJECT: + if self.enum_entity.has_project_override: + return True + + for child_obj in self.non_gui_children[self.current_enum].values(): + if child_obj.has_project_override: + return True + return False + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET From 58ade824c5a49f0f59fcd6ad0d46c24ded5bd7c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:02:45 +0200 Subject: [PATCH 042/333] implemented update current metadata --- .../settings/entities/dict_conditional.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index f82cc02e3e..df7699a90e 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -323,6 +323,45 @@ class DictConditionalEntity(ItemEntity): return "/".join([self.path, result_key]) + def _update_current_metadata(self): + current_metadata = {} + for key, child_obj in self.non_gui_children[self.current_enum].items(): + if self._override_state is OverrideState.DEFAULTS: + break + + if not child_obj.is_group: + continue + + if ( + self._override_state is OverrideState.STUDIO + and not child_obj.has_studio_override + ): + continue + + if ( + self._override_state is OverrideState.PROJECT + and not child_obj.has_project_override + ): + continue + + if M_OVERRIDEN_KEY not in current_metadata: + current_metadata[M_OVERRIDEN_KEY] = [] + current_metadata[M_OVERRIDEN_KEY].append(key) + + # Define if current metadata are avaialble for current override state + metadata = NOT_SET + if self._override_state is OverrideState.STUDIO: + metadata = self._studio_override_metadata + + elif self._override_state is OverrideState.PROJECT: + metadata = self._project_override_metadata + + if metadata is NOT_SET: + metadata = {} + + self._metadata_are_modified = current_metadata != metadata + self._current_metadata = current_metadata + def set_override_state(self, state): # Trigger override state change of root if is not same if self.root_item.override_state is not state: From 1f9ba64a45bbe6b2300436b341b10e985597ca63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:02:55 +0200 Subject: [PATCH 043/333] implemented prepare value --- .../settings/entities/dict_conditional.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index df7699a90e..8172550075 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -474,3 +474,37 @@ class DictConditionalEntity(ItemEntity): output.update(self._current_metadata) return output + + def _prepare_value(self, value): + if value is NOT_SET or self.enum_key not in value: + return NOT_SET, NOT_SET + + enum_value = value.get(self.enum_key) + if enum_value not in self.non_gui_children: + return NOT_SET, NOT_SET + + # Create copy of value before poping values + value = copy.deepcopy(value) + metadata = {} + for key in METADATA_KEYS: + if key in value: + metadata[key] = value.pop(key) + + enum_value = value.get(self.enum_key) + + old_metadata = metadata.get(M_OVERRIDEN_KEY) + if old_metadata: + old_metadata_set = set(old_metadata) + new_metadata = [] + non_gui_children = self.non_gui_children[enum_value] + for key in non_gui_children.keys(): + if key in old_metadata: + new_metadata.append(key) + old_metadata_set.remove(key) + + for key in old_metadata_set: + new_metadata.append(key) + metadata[M_OVERRIDEN_KEY] = new_metadata + + return value, metadata + From f47ec0df6cfe1137e78afa14b27c54b8e4f88e49 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:15:06 +0200 Subject: [PATCH 044/333] implemented update methods --- .../settings/entities/dict_conditional.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8172550075..20697506fd 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -508,3 +508,112 @@ class DictConditionalEntity(ItemEntity): return value, metadata + def update_default_value(self, value): + """Update default values. + + Not an api method, should be called by parent. + """ + value = self._check_update_value(value, "default") + self.has_default_value = value is not NOT_SET + # TODO add value validation + value, metadata = self._prepare_value(value) + self._default_metadata = metadata + + if value is NOT_SET: + self.enum_entity.update_default_value(value) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key: + child_obj.update_default_value(value) + return + + value_keys = set(value.keys()) + enum_value = value[self.enum_key] + expected_keys = set(self.non_gui_children[enum_value]) + expected_keys.add(self.enum_key) + unknown_keys = value_keys - expected_keys + if unknown_keys: + self.log.warning( + "{} Unknown keys in default values: {}".format( + self.path, + ", ".join("\"{}\"".format(key) for key in unknown_keys) + ) + ) + + self.enum_entity.update_default_value(enum_value) + for children_by_key in self.non_gui_children.items(): + for key, child_obj in children_by_key.items(): + child_value = value.get(key, NOT_SET) + child_obj.update_default_value(child_value) + + def update_studio_value(self, value): + """Update studio override values. + + Not an api method, should be called by parent. + """ + value = self._check_update_value(value, "studio override") + value, metadata = self._prepare_value(value) + self._studio_override_metadata = metadata + self.had_studio_override = metadata is not NOT_SET + + if value is NOT_SET: + self.enum_entity.update_default_value(value) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key: + child_obj.update_default_value(value) + return + + value_keys = set(value.keys()) + enum_value = value[self.enum_key] + expected_keys = set(self.non_gui_children[enum_value]) + expected_keys.add(self.enum_key) + unknown_keys = value_keys - expected_keys + if unknown_keys: + self.log.warning( + "{} Unknown keys in studio overrides: {}".format( + self.path, + ", ".join("\"{}\"".format(key) for key in unknown_keys) + ) + ) + + self.enum_entity.update_studio_value(enum_value) + for children_by_key in self.non_gui_children.items(): + for key, child_obj in children_by_key.items(): + child_value = value.get(key, NOT_SET) + child_obj.update_studio_value(child_value) + + def update_project_value(self, value): + """Update project override values. + + Not an api method, should be called by parent. + """ + value = self._check_update_value(value, "project override") + value, metadata = self._prepare_value(value) + self._project_override_metadata = metadata + self.had_project_override = metadata is not NOT_SET + + if value is NOT_SET: + self.enum_entity.update_default_value(value) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key: + child_obj.update_default_value(value) + return + + value_keys = set(value.keys()) + enum_value = value[self.enum_key] + expected_keys = set(self.non_gui_children[enum_value]) + expected_keys.add(self.enum_key) + unknown_keys = value_keys - expected_keys + if unknown_keys: + self.log.warning( + "{} Unknown keys in project overrides: {}".format( + self.path, + ", ".join("\"{}\"".format(key) for key in unknown_keys) + ) + ) + + self.enum_entity.update_project_value(enum_value) + for children_by_key in self.non_gui_children.items(): + for key, child_obj in children_by_key.items(): + child_value = value.get(key, NOT_SET) + child_obj.update_project_value(child_value) + From baf706627af1cfc6a4724897d3ddfda054bcac37 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:15:28 +0200 Subject: [PATCH 045/333] implemented actions for conditional dictionary --- .../settings/entities/dict_conditional.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 20697506fd..5549ce13f2 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -617,3 +617,64 @@ class DictConditionalEntity(ItemEntity): child_value = value.get(key, NOT_SET) child_obj.update_project_value(child_value) + def _discard_changes(self, on_change_trigger): + self._ignore_child_changes = True + + self.enum_entity.discard_changes(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.discard_changes(on_change_trigger) + + self._ignore_child_changes = False + + def _add_to_studio_default(self, on_change_trigger): + self._ignore_child_changes = True + + self.enum_entity.add_to_studio_default(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.add_to_studio_default(on_change_trigger) + + self._ignore_child_changes = False + + self._update_current_metadata() + + self.parent.on_child_change(self) + + def _remove_from_studio_default(self, on_change_trigger): + self._ignore_child_changes = True + + self.enum_entity.remove_from_studio_default(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.remove_from_studio_default(on_change_trigger) + + self._ignore_child_changes = False + + def _add_to_project_override(self, on_change_trigger): + self._ignore_child_changes = True + + self.enum_entity.add_to_project_override(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.add_to_project_override(on_change_trigger) + + self._ignore_child_changes = False + + self._update_current_metadata() + + self.parent.on_child_change(self) + + def _remove_from_project_override(self, on_change_trigger): + if self._override_state is not OverrideState.PROJECT: + return + + self._ignore_child_changes = True + + self.enum_entity.remove_from_project_override(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.remove_from_project_override(on_change_trigger) + + self._ignore_child_changes = False + From f70b19305cd69f4d9755324b97dfc6702970b290 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:16:07 +0200 Subject: [PATCH 046/333] implemented reset_callbacks method --- openpype/settings/entities/dict_conditional.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 5549ce13f2..df852587f6 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -678,3 +678,9 @@ class DictConditionalEntity(ItemEntity): self._ignore_child_changes = False + def reset_callbacks(self): + """Reset registered callbacks on entity and children.""" + super(DictConditionalEntity, self).reset_callbacks() + for children in self.children.values(): + for child_entity in children: + child_entity.reset_callbacks() From 1048a1268cde84d2d0236c78d6eacc7d5f2a25d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:18:02 +0200 Subject: [PATCH 047/333] import DictConditionalEntity in entities init --- openpype/settings/entities/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 94eb819f2b..c0eef15e69 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -111,6 +111,7 @@ from .enum_entity import ( from .list_entity import ListEntity from .dict_immutable_keys_entity import DictImmutableKeysEntity from .dict_mutable_keys_entity import DictMutableKeysEntity +from .dict_conditional import DictConditionalEntity from .anatomy_entities import AnatomyEntity @@ -166,5 +167,7 @@ __all__ = ( "DictMutableKeysEntity", + "DictConditionalEntity", + "AnatomyEntity" ) From e8f7f1418e6a11c510825f7337388b2909b77214 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:32:35 +0200 Subject: [PATCH 048/333] fix example schema --- openpype/settings/entities/dict_conditional.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index df852587f6..e5803b7606 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -57,6 +57,8 @@ example_schema = { "label": "Menu", "children": [ { + "key": "children", + "label": "Children", "type": "list", "object_type": "text" } From 85a3dd1ea6d6742df6eb23d4a8609db3a892c8d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:32:54 +0200 Subject: [PATCH 049/333] fix dict vs. list approach --- openpype/settings/entities/dict_conditional.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index e5803b7606..8fc22348a7 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -524,7 +524,7 @@ class DictConditionalEntity(ItemEntity): if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): - for child_obj in children_by_key: + for child_obj in children_by_key.values(): child_obj.update_default_value(value) return @@ -560,7 +560,7 @@ class DictConditionalEntity(ItemEntity): if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): - for child_obj in children_by_key: + for child_obj in children_by_key.values(): child_obj.update_default_value(value) return @@ -596,7 +596,7 @@ class DictConditionalEntity(ItemEntity): if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): - for child_obj in children_by_key: + for child_obj in children_by_key.values(): child_obj.update_default_value(value) return From 16ac770359d5d9d78e9db11fde41649838e922e1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 19:57:42 +0200 Subject: [PATCH 050/333] created copy of DictConditionalWidget as base for DictConditionalWidget --- .../tools/settings/settings/categories.py | 5 + .../settings/settings/dict_conditional.py | 253 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 openpype/tools/settings/settings/dict_conditional.py diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 34ab4c464a..0dfafce186 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -11,6 +11,7 @@ from openpype.settings.entities import ( GUIEntity, DictImmutableKeysEntity, DictMutableKeysEntity, + DictConditionalEntity, ListEntity, PathEntity, ListStrictEntity, @@ -35,6 +36,7 @@ from .base import GUIWidget from .list_item_widget import ListWidget from .list_strict_widget import ListStrictWidget from .dict_mutable_widget import DictMutableKeysWidget +from .dict_conditional import DictConditionalWidget from .item_widgets import ( BoolWidget, DictImmutableKeysWidget, @@ -100,6 +102,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): if isinstance(entity, GUIEntity): return GUIWidget(*args) + elif isinstance(entity, DictConditionalEntity): + return DictConditionalWidget(*args) + elif isinstance(entity, DictImmutableKeysEntity): return DictImmutableKeysWidget(*args) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py new file mode 100644 index 0000000000..e7e0a31401 --- /dev/null +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -0,0 +1,253 @@ +import collections +from Qt import QtWidgets, QtCore, QtGui + +from .widgets import ( + ExpandingWidget, + GridLabelWidget +) +from .wrapper_widgets import ( + WrapperWidget, + CollapsibleWrapper, + FormWrapper +) +from .base import BaseWidget +from openpype.tools.settings import CHILD_OFFSET + + +class DictConditionalWidget(BaseWidget): + def create_ui(self): + self.input_fields = [] + self.checkbox_child = None + + self.label_widget = None + self.body_widget = None + self.content_widget = None + self.content_layout = None + + label = None + if self.entity.is_dynamic_item: + self._ui_as_dynamic_item() + + elif self.entity.use_label_wrap: + self._ui_label_wrap() + self.checkbox_child = self.entity.non_gui_children.get( + self.entity.checkbox_key + ) + + else: + self._ui_item_base() + label = self.entity.label + + self._parent_widget_by_entity_id = {} + self._added_wrapper_ids = set() + self._prepare_entity_layouts( + self.entity.gui_layout, self.content_widget + ) + + for child_obj in self.entity.children: + self.input_fields.append( + self.create_ui_for_entity( + self.category_widget, child_obj, self + ) + ) + + if self.entity.use_label_wrap and self.content_layout.count() == 0: + self.body_widget.hide_toolbox(True) + + self.entity_widget.add_widget_to_layout(self, label) + + def _prepare_entity_layouts(self, children, widget): + for child in children: + if not isinstance(child, dict): + if child is not self.checkbox_child: + self._parent_widget_by_entity_id[child.id] = widget + continue + + if child["type"] == "collapsible-wrap": + wrapper = CollapsibleWrapper(child, widget) + + elif child["type"] == "form": + wrapper = FormWrapper(child, widget) + + else: + raise KeyError( + "Unknown Wrapper type \"{}\"".format(child["type"]) + ) + + self._parent_widget_by_entity_id[wrapper.id] = widget + + self._prepare_entity_layouts(child["children"], wrapper) + + def _ui_item_base(self): + self.setObjectName("DictInvisible") + + self.content_widget = self + self.content_layout = QtWidgets.QGridLayout(self) + self.content_layout.setContentsMargins(0, 0, 0, 0) + self.content_layout.setSpacing(5) + + def _ui_as_dynamic_item(self): + content_widget = QtWidgets.QWidget(self) + content_widget.setObjectName("DictAsWidgetBody") + + show_borders = str(int(self.entity.show_borders)) + content_widget.setProperty("show_borders", show_borders) + + label_widget = QtWidgets.QLabel(self.entity.label) + + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setContentsMargins(5, 5, 5, 5) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(5) + main_layout.addWidget(content_widget) + + self.label_widget = label_widget + self.content_widget = content_widget + self.content_layout = content_layout + + def _ui_label_wrap(self): + content_widget = QtWidgets.QWidget(self) + content_widget.setObjectName("ContentWidget") + + if self.entity.highlight_content: + content_state = "hightlighted" + bottom_margin = 5 + else: + content_state = "" + bottom_margin = 0 + content_widget.setProperty("content_state", content_state) + content_layout_margins = (CHILD_OFFSET, 5, 0, bottom_margin) + + body_widget = ExpandingWidget(self.entity.label, self) + label_widget = body_widget.label_widget + body_widget.set_content_widget(content_widget) + + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setContentsMargins(*content_layout_margins) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(body_widget) + + self.label_widget = label_widget + self.body_widget = body_widget + self.content_widget = content_widget + self.content_layout = content_layout + + if len(self.input_fields) == 1 and self.checkbox_child: + body_widget.hide_toolbox(hide_content=True) + + elif self.entity.collapsible: + if not self.entity.collapsed: + body_widget.toggle_content() + else: + body_widget.hide_toolbox(hide_content=False) + + def add_widget_to_layout(self, widget, label=None): + if self.checkbox_child and widget.entity is self.checkbox_child: + self.body_widget.add_widget_before_label(widget) + return + + if not widget.entity: + map_id = widget.id + else: + map_id = widget.entity.id + + wrapper = self._parent_widget_by_entity_id[map_id] + if wrapper is not self.content_widget: + wrapper.add_widget_to_layout(widget, label) + if wrapper.id not in self._added_wrapper_ids: + self.add_widget_to_layout(wrapper) + self._added_wrapper_ids.add(wrapper.id) + return + + row = self.content_layout.rowCount() + if not label or isinstance(widget, WrapperWidget): + self.content_layout.addWidget(widget, row, 0, 1, 2) + else: + label_widget = GridLabelWidget(label, widget) + label_widget.input_field = widget + widget.label_widget = label_widget + self.content_layout.addWidget(label_widget, row, 0, 1, 1) + self.content_layout.addWidget(widget, row, 1, 1, 1) + + def set_entity_value(self): + for input_field in self.input_fields: + input_field.set_entity_value() + + def hierarchical_style_update(self): + self.update_style() + for input_field in self.input_fields: + input_field.hierarchical_style_update() + + def update_style(self): + if not self.body_widget and not self.label_widget: + return + + if self.entity.group_item: + group_item = self.entity.group_item + has_unsaved_changes = group_item.has_unsaved_changes + has_project_override = group_item.has_project_override + has_studio_override = group_item.has_studio_override + else: + has_unsaved_changes = self.entity.has_unsaved_changes + has_project_override = self.entity.has_project_override + has_studio_override = self.entity.has_studio_override + + style_state = self.get_style_state( + self.is_invalid, + has_unsaved_changes, + has_project_override, + has_studio_override + ) + if self._style_state == style_state: + return + + self._style_state = style_state + + if self.body_widget: + if style_state: + child_style_state = "child-{}".format(style_state) + else: + child_style_state = "" + + self.body_widget.side_line_widget.setProperty( + "state", child_style_state + ) + self.body_widget.side_line_widget.style().polish( + self.body_widget.side_line_widget + ) + + # There is nothing to care if there is no label + if not self.label_widget: + return + + # Don't change label if is not group or under group item + if not self.entity.is_group and not self.entity.group_item: + return + + self.label_widget.setProperty("state", style_state) + self.label_widget.style().polish(self.label_widget) + + def _on_entity_change(self): + pass + + @property + def is_invalid(self): + return self._is_invalid or self._child_invalid + + @property + def _child_invalid(self): + for input_field in self.input_fields: + if input_field.is_invalid: + return True + return False + + def get_invalid(self): + invalid = [] + for input_field in self.input_fields: + invalid.extend(input_field.get_invalid()) + return invalid From c3614dbfce046c692968399a9e7433613bff6655 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 19:58:29 +0200 Subject: [PATCH 051/333] removed checkbox checks as checkbox is not available for conditional dict --- .../tools/settings/settings/dict_conditional.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index e7e0a31401..2024bd3258 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -17,7 +17,6 @@ from openpype.tools.settings import CHILD_OFFSET class DictConditionalWidget(BaseWidget): def create_ui(self): self.input_fields = [] - self.checkbox_child = None self.label_widget = None self.body_widget = None @@ -30,9 +29,6 @@ class DictConditionalWidget(BaseWidget): elif self.entity.use_label_wrap: self._ui_label_wrap() - self.checkbox_child = self.entity.non_gui_children.get( - self.entity.checkbox_key - ) else: self._ui_item_base() @@ -59,8 +55,7 @@ class DictConditionalWidget(BaseWidget): def _prepare_entity_layouts(self, children, widget): for child in children: if not isinstance(child, dict): - if child is not self.checkbox_child: - self._parent_widget_by_entity_id[child.id] = widget + parent_widget_by_entity_id[child.id] = widget continue if child["type"] == "collapsible-wrap": @@ -137,20 +132,13 @@ class DictConditionalWidget(BaseWidget): self.content_widget = content_widget self.content_layout = content_layout - if len(self.input_fields) == 1 and self.checkbox_child: - body_widget.hide_toolbox(hide_content=True) - - elif self.entity.collapsible: + if self.entity.collapsible: if not self.entity.collapsed: body_widget.toggle_content() else: body_widget.hide_toolbox(hide_content=False) def add_widget_to_layout(self, widget, label=None): - if self.checkbox_child and widget.entity is self.checkbox_child: - self.body_widget.add_widget_before_label(widget) - return - if not widget.entity: map_id = widget.id else: From 1b1ce1f2a5602b07714f19e8aeb0404ba072f7f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 19:59:07 +0200 Subject: [PATCH 052/333] modified _prepare_entity_layouts to be able to store result into passed dictionary --- openpype/tools/settings/settings/dict_conditional.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 2024bd3258..aeb2b7d86c 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -52,8 +52,10 @@ class DictConditionalWidget(BaseWidget): self.entity_widget.add_widget_to_layout(self, label) - def _prepare_entity_layouts(self, children, widget): - for child in children: + def _prepare_entity_layouts( + self, gui_layout, widget, parent_widget_by_entity_id + ): + for child in gui_layout: if not isinstance(child, dict): parent_widget_by_entity_id[child.id] = widget continue @@ -69,9 +71,11 @@ class DictConditionalWidget(BaseWidget): "Unknown Wrapper type \"{}\"".format(child["type"]) ) - self._parent_widget_by_entity_id[wrapper.id] = widget + parent_widget_by_entity_id[wrapper.id] = widget - self._prepare_entity_layouts(child["children"], wrapper) + self._prepare_entity_layouts( + child["children"], wrapper, parent_widget_by_entity_id + ) def _ui_item_base(self): self.setObjectName("DictInvisible") From 9131982be41add0330eaef9f0812e056eb194c13 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 19:59:37 +0200 Subject: [PATCH 053/333] added few required attributes --- openpype/tools/settings/settings/dict_conditional.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index aeb2b7d86c..47c1d7d4c9 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -18,6 +18,9 @@ class DictConditionalWidget(BaseWidget): def create_ui(self): self.input_fields = [] + self._content_by_enum_value = {} + self._last_enum_value = None + self.label_widget = None self.body_widget = None self.content_widget = None @@ -35,6 +38,7 @@ class DictConditionalWidget(BaseWidget): label = self.entity.label self._parent_widget_by_entity_id = {} + self._enum_key_by_wrapper_id = {} self._added_wrapper_ids = set() self._prepare_entity_layouts( self.entity.gui_layout, self.content_widget From 84f725b36447aca06ed676a2df3b44ede503dc8d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:02:02 +0200 Subject: [PATCH 054/333] modified how preparation of layout works --- .../settings/settings/dict_conditional.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 47c1d7d4c9..2287e52595 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -40,9 +40,22 @@ class DictConditionalWidget(BaseWidget): self._parent_widget_by_entity_id = {} self._enum_key_by_wrapper_id = {} self._added_wrapper_ids = set() - self._prepare_entity_layouts( - self.entity.gui_layout, self.content_widget - ) + + # Add enum entity to layout mapping + enum_entity = self.entity.enum_entity + self._parent_widget_by_entity_id[enum_entity.id] = self.content_widget + + # Add rest of entities to wrapper mappings + for enum_key, children in self.entity.gui_layout.items(): + parent_widget_by_entity_id = {} + self._prepare_entity_layouts( + children, + self.content_widget, + parent_widget_by_entity_id + ) + for item_id in parent_widget_by_entity_id.keys(): + self._enum_key_by_wrapper_id[item_id] = enum_key + self._parent_widget_by_entity_id.update(parent_widget_by_entity_id) for child_obj in self.entity.children: self.input_fields.append( From 2ebd5daac3be15bd3cc3feb68f863191f653df89 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:02:56 +0200 Subject: [PATCH 055/333] store content of each enum key to different widget --- .../tools/settings/settings/dict_conditional.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 2287e52595..c2e59e0fbe 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -48,9 +48,22 @@ class DictConditionalWidget(BaseWidget): # Add rest of entities to wrapper mappings for enum_key, children in self.entity.gui_layout.items(): parent_widget_by_entity_id = {} + + content_widget = QtWidgets.QWidget(self.content_widget) + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setColumnStretch(0, 0) + content_layout.setColumnStretch(1, 1) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(5) + + self._content_by_enum_value[enum_key] = { + "widget": content_widget, + "layout": content_layout + } + self._prepare_entity_layouts( children, - self.content_widget, + content_widget, parent_widget_by_entity_id ) for item_id in parent_widget_by_entity_id.keys(): From cc72287ddbccb400edea8f25d6ac1850f2359609 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:03:26 +0200 Subject: [PATCH 056/333] modified how entity widgets are created and when --- .../settings/settings/dict_conditional.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index c2e59e0fbe..3798cffe38 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -70,12 +70,23 @@ class DictConditionalWidget(BaseWidget): self._enum_key_by_wrapper_id[item_id] = enum_key self._parent_widget_by_entity_id.update(parent_widget_by_entity_id) - for child_obj in self.entity.children: - self.input_fields.append( - self.create_ui_for_entity( - self.category_widget, child_obj, self + enum_input_field = self.create_ui_for_entity( + self.category_widget, self.entity.enum_entity, self + ) + self.enum_input_field = enum_input_field + self.input_fields.append(enum_input_field) + + for item_key, children in self.entity.children.items(): + content_widget = self._content_by_enum_value[item_key]["widget"] + row = self.content_layout.rowCount() + self.content_layout.addWidget(content_widget, row, 0, 1, 2) + + for child_obj in children: + self.input_fields.append( + self.create_ui_for_entity( + self.category_widget, child_obj, self + ) ) - ) if self.entity.use_label_wrap and self.content_layout.count() == 0: self.body_widget.hide_toolbox(True) From c8a5de88bd5918a302132e0e37fa1bc362a41b7b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:03:52 +0200 Subject: [PATCH 057/333] define content widget based on map_id and entity id --- .../tools/settings/settings/dict_conditional.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 3798cffe38..013fefb74f 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -189,23 +189,30 @@ class DictConditionalWidget(BaseWidget): else: map_id = widget.entity.id + content_widget = self.content_widget + content_layout = self.content_layout + if map_id != self.entity.enum_entity.id: + enum_value = self._enum_key_by_wrapper_id[map_id] + content_widget = self._content_by_enum_value[enum_value]["widget"] + content_layout = self._content_by_enum_value[enum_value]["layout"] + wrapper = self._parent_widget_by_entity_id[map_id] - if wrapper is not self.content_widget: + if wrapper is not content_widget: wrapper.add_widget_to_layout(widget, label) if wrapper.id not in self._added_wrapper_ids: self.add_widget_to_layout(wrapper) self._added_wrapper_ids.add(wrapper.id) return - row = self.content_layout.rowCount() + row = content_layout.rowCount() if not label or isinstance(widget, WrapperWidget): - self.content_layout.addWidget(widget, row, 0, 1, 2) + content_layout.addWidget(widget, row, 0, 1, 2) else: label_widget = GridLabelWidget(label, widget) label_widget.input_field = widget widget.label_widget = label_widget - self.content_layout.addWidget(label_widget, row, 0, 1, 1) - self.content_layout.addWidget(widget, row, 1, 1, 1) + content_layout.addWidget(label_widget, row, 0, 1, 1) + content_layout.addWidget(widget, row, 1, 1, 1) def set_entity_value(self): for input_field in self.input_fields: From 313a78a3918cf279c0feceac64480b47afeb927e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:04:11 +0200 Subject: [PATCH 058/333] trigger change of visibility on change of enum --- openpype/tools/settings/settings/dict_conditional.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 013fefb74f..5442af14b4 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -273,7 +273,14 @@ class DictConditionalWidget(BaseWidget): self.label_widget.style().polish(self.label_widget) def _on_entity_change(self): - pass + enum_value = self.enum_input_field.entity.value + if enum_value == self._last_enum_value: + return + + self._last_enum_value = enum_value + for item_key, content in self._content_by_enum_value.items(): + widget = content["widget"] + widget.setVisible(item_key == enum_value) @property def is_invalid(self): From b344e0c27ad71350c887f2d90c3a2c5fe7ecb031 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:04:32 +0200 Subject: [PATCH 059/333] set_entity_value triggers on entity change --- openpype/tools/settings/settings/dict_conditional.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 5442af14b4..05dfa47e60 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -218,6 +218,8 @@ class DictConditionalWidget(BaseWidget): for input_field in self.input_fields: input_field.set_entity_value() + self._on_entity_change() + def hierarchical_style_update(self): self.update_style() for input_field in self.input_fields: From c9ee4e5f713f20dec0d4f684a596147b71f58850 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:04:45 +0200 Subject: [PATCH 060/333] added column stretch to grid layout --- openpype/tools/settings/settings/dict_conditional.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 05dfa47e60..84288f7b5b 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -41,6 +41,9 @@ class DictConditionalWidget(BaseWidget): self._enum_key_by_wrapper_id = {} self._added_wrapper_ids = set() + self.content_layout.setColumnStretch(0, 0) + self.content_layout.setColumnStretch(1, 1) + # Add enum entity to layout mapping enum_entity = self.entity.enum_entity self._parent_widget_by_entity_id[enum_entity.id] = self.content_widget From e1129bdbad5a167e4428bd3d298e1e4e5012fdd9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:25:45 +0200 Subject: [PATCH 061/333] fix keys method --- openpype/settings/entities/dict_conditional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8fc22348a7..f72d1c8b82 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -105,7 +105,7 @@ class DictConditionalEntity(ItemEntity): def keys(self): """Entity's keys.""" keys = list(self.non_gui_children[self.current_enum].keys()) - keys.insert(0, [self.current_enum]) + keys.insert(0, [self.enum_key]) return keys def values(self): From 4bff4a8138506f1fdbe247e54d4fb739bb111bff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:29:07 +0200 Subject: [PATCH 062/333] force to be a group --- openpype/settings/entities/dict_conditional.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index f72d1c8b82..2e8cd6affe 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -149,6 +149,13 @@ class DictConditionalEntity(ItemEntity): self._current_metadata = {} self._metadata_are_modified = False + if ( + self.group_item is None + and not self.is_dynamic_item + and not self.is_in_dynamic_item + ): + self.is_group = True + # Children are stored by key as keys are immutable and are defined by # schema self.valid_value_types = (dict, ) From 40f87f2f20c5fee45f2be2e903aaf9814f9acf43 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:30:11 +0200 Subject: [PATCH 063/333] few minor fixes of entity --- .../settings/entities/dict_conditional.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 2e8cd6affe..d3aad60df6 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -391,7 +391,7 @@ class DictConditionalEntity(ItemEntity): @property def value(self): output = { - self.current_enum: self.enum_entity.value + self.enum_key: self.enum_entity.value } for key, child_obj in self.non_gui_children[self.current_enum].items(): output[key] = child_obj.value @@ -408,6 +408,7 @@ class DictConditionalEntity(ItemEntity): def _child_has_unsaved_changes(self): if self.enum_entity.has_unsaved_changes: return True + for child_obj in self.non_gui_children[self.current_enum].values(): if child_obj.has_unsaved_changes: return True @@ -448,11 +449,14 @@ class DictConditionalEntity(ItemEntity): return NOT_SET if self._override_state is OverrideState.DEFAULTS: - output = { - self.current_enum: self.enum_entity.settings_value() - } - non_gui_children = self.non_gui_children[self.current_enum] - for key, child_obj in non_gui_children.items(): + children_items = [ + (self.enum_key, self.enum_entity) + ] + for item in self.non_gui_children[self.current_enum].items(): + children_items.append(item) + + output = {} + for key, child_obj in children_items: child_value = child_obj.settings_value() if not child_obj.is_file and not child_obj.file_item: for _key, _value in child_value.items(): @@ -470,10 +474,14 @@ class DictConditionalEntity(ItemEntity): if not self.has_project_override: return NOT_SET - output = { - self.current_enum: self.enum_entity.settings_value() - } - for key, child_obj in self.non_gui_children[self.current_enum].items(): + output = {} + children_items = [ + (self.enum_key, self.enum_entity) + ] + for item in self.non_gui_children[self.current_enum].items(): + children_items.append(item) + + for key, child_obj in children_items: value = child_obj.settings_value() if value is not NOT_SET: output[key] = value @@ -537,7 +545,7 @@ class DictConditionalEntity(ItemEntity): value_keys = set(value.keys()) enum_value = value[self.enum_key] - expected_keys = set(self.non_gui_children[enum_value]) + expected_keys = set(self.non_gui_children[enum_value].keys()) expected_keys.add(self.enum_key) unknown_keys = value_keys - expected_keys if unknown_keys: @@ -549,7 +557,7 @@ class DictConditionalEntity(ItemEntity): ) self.enum_entity.update_default_value(enum_value) - for children_by_key in self.non_gui_children.items(): + for children_by_key in self.non_gui_children.values(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_default_value(child_value) @@ -565,10 +573,10 @@ class DictConditionalEntity(ItemEntity): self.had_studio_override = metadata is not NOT_SET if value is NOT_SET: - self.enum_entity.update_default_value(value) + self.enum_entity.update_studio_value(value) for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): - child_obj.update_default_value(value) + child_obj.update_studio_value(value) return value_keys = set(value.keys()) @@ -601,10 +609,10 @@ class DictConditionalEntity(ItemEntity): self.had_project_override = metadata is not NOT_SET if value is NOT_SET: - self.enum_entity.update_default_value(value) + self.enum_entity.update_project_value(value) for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): - child_obj.update_default_value(value) + child_obj.update_project_value(value) return value_keys = set(value.keys()) From 3d59ba17d54600247593a7c05c00a0576e124dc1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:37:04 +0200 Subject: [PATCH 064/333] current_enum is dynamic property --- openpype/settings/entities/dict_conditional.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d3aad60df6..6802af5806 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -171,10 +171,15 @@ class DictConditionalEntity(ItemEntity): self.enum_children = self.schema_data.get("enum_children") self.enum_entity = None - self.current_enum = None self._add_children() + @property + def current_enum(self): + if self.enum_entity is None: + return None + return self.enum_entity.value + def schema_validations(self): """Validation of schema data.""" if self.enum_key is None: @@ -269,25 +274,20 @@ class DictConditionalEntity(ItemEntity): if not self.enum_children or not self.enum_key: return - enum_items = [] valid_enum_items = [] for item in self.enum_children: if isinstance(item, dict) and "key" in item: valid_enum_items.append(item) - first_key = None + enum_items = [] for item in valid_enum_items: item_key = item["key"] - if first_key is None: - first_key = item_key item_label = item.get("label") or item_key enum_items.append({item_key: item_label}) if not enum_items: return - self.current_enum = first_key - enum_key = self.enum_key or "invalid" enum_schema = { "type": "enum", @@ -296,6 +296,7 @@ class DictConditionalEntity(ItemEntity): "key": enum_key, "label": self.enum_label or enum_key } + enum_entity = self.create_schema_object(enum_schema, self) self.enum_entity = enum_entity From f3ae791f5c0afc283a5a17490bb70bf362e4a1e4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:56:13 +0200 Subject: [PATCH 065/333] make sure all keys are available in all variables --- openpype/settings/entities/dict_conditional.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 6802af5806..956180c3da 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -159,9 +159,9 @@ class DictConditionalEntity(ItemEntity): # Children are stored by key as keys are immutable and are defined by # schema self.valid_value_types = (dict, ) - self.children = collections.defaultdict(list) - self.non_gui_children = collections.defaultdict(dict) - self.gui_layout = collections.defaultdict(list) + self.children = {} + self.non_gui_children = {} + self.gui_layout = {} if self.is_dynamic_item: self.require_key = False @@ -302,6 +302,11 @@ class DictConditionalEntity(ItemEntity): for item in valid_enum_items: item_key = item["key"] + # Make sure all keys have set value in there variables + self.non_gui_children[item_key] = {} + self.children[item_key] = [] + self.gui_layout[item_key] = [] + children = item.get("children") or [] for children_schema in children: child_obj = self.create_schema_object(children_schema, self) From 0e0e527741392bad6a67de9f3d2736521a149326 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 12:44:50 +0200 Subject: [PATCH 066/333] items vs. values fix --- openpype/settings/entities/dict_conditional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 956180c3da..858c2ca4e8 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -599,7 +599,7 @@ class DictConditionalEntity(ItemEntity): ) self.enum_entity.update_studio_value(enum_value) - for children_by_key in self.non_gui_children.items(): + for children_by_key in self.non_gui_children.values(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_studio_value(child_value) @@ -635,7 +635,7 @@ class DictConditionalEntity(ItemEntity): ) self.enum_entity.update_project_value(enum_value) - for children_by_key in self.non_gui_children.items(): + for children_by_key in self.non_gui_children.values(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_project_value(child_value) From d58c8f1a112a0daaf2e3227794923d0262ee344f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 12:46:10 +0200 Subject: [PATCH 067/333] added argument ignore_missing_defaults to set_override_state method --- openpype/settings/entities/base_entity.py | 9 ++++++++- openpype/settings/entities/dict_conditional.py | 2 +- .../entities/dict_immutable_keys_entity.py | 4 ++-- .../entities/dict_mutable_keys_entity.py | 14 ++++++++++---- openpype/settings/entities/input_entities.py | 12 +++++++++--- openpype/settings/entities/item_entities.py | 18 ++++++++++++------ openpype/settings/entities/list_entity.py | 16 ++++++++++++---- openpype/settings/entities/root_entities.py | 7 +++++-- 8 files changed, 59 insertions(+), 23 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 0e29a35e1f..e1cd5134e7 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -280,7 +280,7 @@ class BaseItemEntity(BaseEntity): ) @abstractmethod - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): """Set override state and trigger it on children. Method discard all changes in hierarchy and use values, metadata @@ -290,8 +290,15 @@ class BaseItemEntity(BaseEntity): Should start on root entity and when triggered then must be called on all entities in hierarchy. + Argument `ignore_missing_defaults` should be used when entity has + children that are not saved or used all the time but override statu + must be changed and children must have any default value. + Args: state (OverrideState): State to which should be data changed. + ignore_missing_defaults (bool): Ignore missing default values. + Entity won't raise `DefaultsNotDefined` and + `StudioDefaultsNotDefined`. """ pass diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 858c2ca4e8..98aa10dacb 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -377,7 +377,7 @@ class DictConditionalEntity(ItemEntity): self._metadata_are_modified = current_metadata != metadata self._current_metadata = current_metadata - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index c965dc3b5a..2802290e68 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -258,7 +258,7 @@ class DictImmutableKeysEntity(ItemEntity): self._metadata_are_modified = current_metadata != metadata self._current_metadata = current_metadata - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -268,7 +268,7 @@ class DictImmutableKeysEntity(ItemEntity): self._override_state = state for child_obj in self.non_gui_children.values(): - child_obj.set_override_state(state) + child_obj.set_override_state(state, ignore_missing_defaults) self._update_current_metadata() diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index 3c2645e3e5..a5734e36b8 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -320,7 +320,7 @@ class DictMutableKeysEntity(EndpointEntity): def _metadata_for_current_state(self): return self._get_metadata_for_state(self._override_state) - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -331,11 +331,17 @@ class DictMutableKeysEntity(EndpointEntity): # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: - if not self.has_default_value: + if ( + not self.has_default_value + and not ignore_missing_defaults + ): raise DefaultsNotDefined(self) elif state > OverrideState.STUDIO: - if not self.had_studio_override: + if ( + not self.had_studio_override + and not ignore_missing_defaults + ): raise StudioDefaultsNotDefined(self) if state is OverrideState.STUDIO: @@ -426,7 +432,7 @@ class DictMutableKeysEntity(EndpointEntity): if label: children_label_by_id[child_entity.id] = label - child_entity.set_override_state(state) + child_entity.set_override_state(state, ignore_missing_defaults) self.children_label_by_id = children_label_by_id diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 295333eb60..9b41a26bdb 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -217,7 +217,7 @@ class InputEntity(EndpointEntity): return True return False - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -227,11 +227,17 @@ class InputEntity(EndpointEntity): # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: - if not self.has_default_value: + if ( + not self.has_default_value + and not ignore_missing_defaults + ): raise DefaultsNotDefined(self) elif state > OverrideState.STUDIO: - if not self.had_studio_override: + if ( + not self.had_studio_override + and not ignore_missing_defaults + ): raise StudioDefaultsNotDefined(self) if state is OverrideState.STUDIO: diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 48336080b6..c52eab988f 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -150,14 +150,14 @@ class PathEntity(ItemEntity): def value(self): return self.child_obj.value - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) return self._override_state = state - self.child_obj.set_override_state(state) + self.child_obj.set_override_state(state, ignore_missing_defaults) def update_default_value(self, value): self.child_obj.update_default_value(value) @@ -344,7 +344,7 @@ class ListStrictEntity(ItemEntity): return True return False - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -354,15 +354,21 @@ class ListStrictEntity(ItemEntity): # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: - if not self.has_default_value: + if ( + not self.has_default_value + and not ignore_missing_defaults + ): raise DefaultsNotDefined(self) elif state > OverrideState.STUDIO: - if not self.had_studio_override: + if ( + not self.had_studio_override + and not ignore_missing_defaults + ): raise StudioDefaultsNotDefined(self) for child_entity in self.children: - child_entity.set_override_state(state) + child_entity.set_override_state(state, ignore_missing_defaults) self.initial_value = self.settings_value() diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index 4b3f7a2659..2225523792 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -205,7 +205,7 @@ class ListEntity(EndpointEntity): self._has_project_override = True self.on_change() - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -219,11 +219,17 @@ class ListEntity(EndpointEntity): # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: - if not self.has_default_value: + if ( + not self.has_default_value + and not ignore_missing_defaults + ): raise DefaultsNotDefined(self) elif state > OverrideState.STUDIO: - if not self.had_studio_override: + if ( + not self.had_studio_override + and not ignore_missing_defaults + ): raise StudioDefaultsNotDefined(self) value = NOT_SET @@ -257,7 +263,9 @@ class ListEntity(EndpointEntity): child_obj.update_studio_value(item) for child_obj in self.children: - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, ignore_missing_defaults + ) self.initial_value = self.settings_value() diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 1833535a07..b758e30cbe 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -218,7 +218,7 @@ class RootEntity(BaseItemEntity): schema_data, *args, **kwargs ) - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults=None): """Set override state and trigger it on children. Method will discard all changes in hierarchy and use values, metadata @@ -227,9 +227,12 @@ class RootEntity(BaseItemEntity): Args: state (OverrideState): State to which should be data changed. """ + if not ignore_missing_defaults: + ignore_missing_defaults = False + self._override_state = state for child_obj in self.non_gui_children.values(): - child_obj.set_override_state(state) + child_obj.set_override_state(state, ignore_missing_defaults) def on_change(self): """Trigger callbacks on change.""" From 0ed3a2ee701f26fc9cdebe19ceefb225848ae38c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 12:47:12 +0200 Subject: [PATCH 068/333] Use ignore missing defaults in conditional dictionary children that are not using current enum value --- openpype/settings/entities/dict_conditional.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 98aa10dacb..112ef8bddc 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -386,11 +386,17 @@ class DictConditionalEntity(ItemEntity): # Change has/had override states self._override_state = state - self.enum_entity.set_override_state(state) + self.enum_entity.set_override_state(state, ignore_missing_defaults) + + for child_obj in self.non_gui_children[self.current_enum].values(): + child_obj.set_override_state(state, ignore_missing_defaults) + + for item_key, children_by_key in self.non_gui_children.items(): + if item_key == self.current_enum: + continue - for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): - child_obj.set_override_state(state) + child_obj.set_override_state(state, True) self._update_current_metadata() From c1d6db4356e7258970dbffb9152a82f408bab025 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 13:04:52 +0200 Subject: [PATCH 069/333] added few comments and docstring --- .../settings/entities/dict_conditional.py | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 112ef8bddc..6e28cbd591 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -73,6 +73,19 @@ example_schema = { class DictConditionalEntity(ItemEntity): + """Entity represents dictionay with only one persistent key definition. + + The persistent key is enumerator which define rest of children under + dictionary. There is not possibility of shared children. + + Entity's keys can't be removed or added. But they may change based on + the persistent key. If you're change value manually (key by key) make sure + you'll change value of the persistent key as first. It is recommended to + use `set` method which handle this for you. + + It is possible to use entity similar way as `dict` object. Returned values + are not real settings values but entities representing the value. + """ schema_types = ["dict-conditional"] _default_label_wrap = { "use_label_wrap": False, @@ -149,6 +162,7 @@ class DictConditionalEntity(ItemEntity): self._current_metadata = {} self._metadata_are_modified = False + # Entity must be group or in group if ( self.group_item is None and not self.is_dynamic_item @@ -176,15 +190,21 @@ class DictConditionalEntity(ItemEntity): @property def current_enum(self): + """Current value of enum entity. + + This value define what children are used. + """ if self.enum_entity is None: return None return self.enum_entity.value def schema_validations(self): """Validation of schema data.""" + # Enum key must be defined if self.enum_key is None: raise EntitySchemaError(self, "Key 'enum_key' is not set.") + # Validate type of enum children if not isinstance(self.enum_children, list): raise EntitySchemaError( self, "Key 'enum_children' must be a list. Got: {}".format( @@ -192,6 +212,7 @@ class DictConditionalEntity(ItemEntity): ) ) + # Without defined enum children entity has nothing to do if not self.enum_children: raise EntitySchemaError(self, ( "Key 'enum_children' have empty value. Entity can't work" @@ -219,6 +240,7 @@ class DictConditionalEntity(ItemEntity): raise SchemaDuplicatedKeys(self, key) children_def_keys.append(key) + # Validate key duplications per each enum item for children in self.children.values(): children_keys = set() children_keys.add(self.enum_key) @@ -230,6 +252,7 @@ class DictConditionalEntity(ItemEntity): else: raise SchemaDuplicatedKeys(self, child_entity.key) + # Validate all remaining keys with key regex for children_by_key in self.non_gui_children.values(): for key in children_by_key.keys(): if not KEY_REGEX.match(key): @@ -270,7 +293,8 @@ class DictConditionalEntity(ItemEntity): All children are stored by their enum item. """ - # Skip and wait for validation + # Skip if are not defined + # - schema validations should raise and exception if not self.enum_children or not self.enum_key: return @@ -288,6 +312,7 @@ class DictConditionalEntity(ItemEntity): if not enum_items: return + # Create Enum child first enum_key = self.enum_key or "invalid" enum_schema = { "type": "enum", @@ -300,9 +325,11 @@ class DictConditionalEntity(ItemEntity): enum_entity = self.create_schema_object(enum_schema, self) self.enum_entity = enum_entity + # Create children per each enum item for item in valid_enum_items: item_key = item["key"] - # Make sure all keys have set value in there variables + # Make sure all keys have set value in these variables + # - key 'children' is optional self.non_gui_children[item_key] = {} self.children[item_key] = [] self.gui_layout[item_key] = [] @@ -386,11 +413,15 @@ class DictConditionalEntity(ItemEntity): # Change has/had override states self._override_state = state + # Set override state on enum entity first self.enum_entity.set_override_state(state, ignore_missing_defaults) + # Set override state on other entities under current enum value for child_obj in self.non_gui_children[self.current_enum].values(): child_obj.set_override_state(state, ignore_missing_defaults) + # Set override state on other enum children + # - these must not raise exception about missing defaults for item_key, children_by_key in self.non_gui_children.items(): if item_key == self.current_enum: continue From 3b217c57a2c8b2069b4cf3e4f5a03acbe66e90ff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 13:05:02 +0200 Subject: [PATCH 070/333] added enum key validation --- openpype/settings/entities/dict_conditional.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 6e28cbd591..96e6c518f3 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -252,6 +252,10 @@ class DictConditionalEntity(ItemEntity): else: raise SchemaDuplicatedKeys(self, child_entity.key) + # Enum key must match key regex + if not KEY_REGEX.match(self.enum_key): + raise InvalidKeySymbols(self.path, self.enum_key) + # Validate all remaining keys with key regex for children_by_key in self.non_gui_children.values(): for key in children_by_key.keys(): From 605a0454b2c69ce3e76a4df85eced6e0364ae47a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 13:05:20 +0200 Subject: [PATCH 071/333] include enum_key in builtin methods --- openpype/settings/entities/dict_conditional.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 96e6c518f3..9ba24cf0de 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -95,11 +95,16 @@ class DictConditionalEntity(ItemEntity): def __getitem__(self, key): """Return entity inder key.""" + if key == self.enum_key: + return self.enum_entity return self.non_gui_children[self.current_enum][key] def __setitem__(self, key, value): """Set value of item under key.""" - child_obj = self.non_gui_children[self.current_enum][key] + if key == self.enum_key: + child_obj = self.enum_entity + else: + child_obj = self.non_gui_children[self.current_enum][key] child_obj.set(value) def __iter__(self): @@ -109,10 +114,14 @@ class DictConditionalEntity(ItemEntity): def __contains__(self, key): """Check if key is available.""" + if key == self.enum_key: + return True return key in self.non_gui_children[self.current_enum] def get(self, key, default=None): """Safe entity getter by key.""" + if key == self.enum_key: + return self.enum_entity return self.non_gui_children[self.current_enum].get(key, default) def keys(self): From 082e453d138055a86c25e8e2ad09060f28ff5d11 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:24:08 +0200 Subject: [PATCH 072/333] fix variable name usage --- openpype/tools/settings/settings/item_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index b23372e9ac..82afbb0a13 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -145,7 +145,7 @@ class DictImmutableKeysWidget(BaseWidget): self.content_widget = content_widget self.content_layout = content_layout - if len(self.input_fields) == 1 and self.checkbox_widget: + if len(self.input_fields) == 1 and self.checkbox_child: body_widget.hide_toolbox(hide_content=True) elif self.entity.collapsible: From 905db947bbf3ae5d53aade3c16f1b2de08c63def Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:40:49 +0200 Subject: [PATCH 073/333] added dict-conditional to readme --- openpype/settings/entities/schemas/README.md | 97 ++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index bbd53fa46b..3c360b892f 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -181,6 +181,103 @@ } ``` +## dict-conditional +- is similar to `dict` but has only one child entity that will be always available +- the one entity is enumerator of possible values and based on value of the entity are defined and used other children entities +- each value of enumerator have defined children that will be used + - there is no way how to have shared entities across multiple enum items +- value from enumerator is also stored next to other values + - to define the key under which will be enum value stored use `enum_key` + - `enum_key` must match key regex and any enum item can't have children with same key + - `enum_label` is label of the entity for UI purposes +- enum items are define with `enum_children` + - it's a list where each item represents enum item + - all items in `enum_children` must have at least `key` key which represents value stored under `enum_key` + - items can define `label` for UI purposes + - most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`) +- entity must have defined `"label"` if is not used as widget +- is set as group if any parent is not group +- if `"label"` is entetered there which will be shown in GUI + - item with label can be collapsible + - that can be set with key `"collapsible"` as `True`/`False` (Default: `True`) + - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) + - it is possible to add darker background with `"highlight_content"` (Default: `False`) + - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color + - output is dictionary `{the "key": children values}` +``` +# Example +{ + "type": "dict-conditional", + "key": "my_key", + "label": "My Key", + "enum_key": "type", + "enum_label": "label", + "enum_children": [ + # Each item must be a dictionary with 'key' + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "command", + "label": "Comand" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "key": "children", + "label": "Children", + "type": "list", + "object_type": "text" + } + ] + }, + { + # Separator does not have children as "separator" value is enough + "key": "separator", + "label": "Separator" + } + ] +} +``` + +How output of the schema could look like on save: +``` +{ + "type": "separator" +} + +{ + "type": "action", + "key": "action_1", + "label": "Action 1", + "command": "run command -arg" +} + +{ + "type": "menu", + "children": [ + "child_1", + "child_2" + ] +} +``` + ## Inputs for setting any kind of value (`Pure` inputs) - all these input must have defined `"key"` under which will be stored and `"label"` which will be shown next to input - unless they are used in different types of inputs (later) "as widgets" in that case `"key"` and `"label"` are not required as there is not place where to set them From df37c2e1fffbeea1baf66c5061ac2699ed9d5a7c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:50:07 +0200 Subject: [PATCH 074/333] removed example schema --- .../settings/entities/dict_conditional.py | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 9ba24cf0de..0e6540e606 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -24,54 +24,6 @@ from .exceptions import ( ) -example_schema = { - "type": "dict-conditional", - "key": "KEY", - "label": "LABEL", - "enum_key": "type", - "enum_label": "label", - "enum_children": [ - { - "key": "action", - "label": "Action", - "children": [ - { - "type": "text", - "key": "key", - "label": "Key" - }, - { - "type": "text", - "key": "label", - "label": "Label" - }, - { - "type": "text", - "key": "command", - "label": "Comand" - } - ] - }, - { - "key": "menu", - "label": "Menu", - "children": [ - { - "key": "children", - "label": "Children", - "type": "list", - "object_type": "text" - } - ] - }, - { - "key": "separator", - "label": "Separator" - } - ] -} - - class DictConditionalEntity(ItemEntity): """Entity represents dictionay with only one persistent key definition. From 11e8e3a8cb269d7d6de06aca6d493f7f8258fc16 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 29 Jun 2021 16:43:20 +0200 Subject: [PATCH 075/333] drop support for <4.26 --- openpype/hosts/unreal/README.md | 9 ++ openpype/hosts/unreal/api/lib.py | 122 +++++------------- .../unreal/hooks/pre_workfile_preparation.py | 23 ++-- .../system_settings/applications.json | 2 +- .../schema_project_unreal.json | 5 - 5 files changed, 55 insertions(+), 106 deletions(-) create mode 100644 openpype/hosts/unreal/README.md diff --git a/openpype/hosts/unreal/README.md b/openpype/hosts/unreal/README.md new file mode 100644 index 0000000000..0a69b9e0cf --- /dev/null +++ b/openpype/hosts/unreal/README.md @@ -0,0 +1,9 @@ +## Unreal Integration + +Supported Unreal Engine version is 4.26+ (mainly because of major Python changes done there). + +### Project naming +Unreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are +invalid. If OpenPype detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` +will become `P123_myProject`. There is also soft-limit on project name length to be shorter than 20 characters. +Longer names will issue warning in Unreal Editor that there might be possible side effects. \ No newline at end of file diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 6231fd6f33..144f781171 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -70,6 +70,21 @@ def get_engine_versions(env=None): return OrderedDict() +def get_editor_executable_path(engine_path: Path) -> Path: + """Get UE4 Editor executable path.""" + ue4_path = engine_path / "Engine/Binaries" + if platform.system().lower() == "windows": + ue4_path /= "Win64/UE4Editor.exe" + + elif platform.system().lower() == "linux": + ue4_path /= "Linux/UE4Editor" + + elif platform.system().lower() == "darwin": + ue4_path /= "Mac/UE4Editor" + + return ue4_path + + def _win_get_engine_versions(): """Get Unreal Engine versions on Windows. @@ -244,39 +259,6 @@ def create_unreal_project(project_name: str, ] } - if preset["install_unreal_python_engine"]: - # WARNING: This is deprecated as Unreal Engine Python project - # is on hold and is mainly replaced in 4.26 by Epics own - # Python implementation. - # --------------------------------------------------------------- - # If `OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from - # there to support offline installation. - # Otherwise clone UnrealEnginePython to Plugins directory - # https://github.com/20tab/UnrealEnginePython.git - uep_path = plugins_path / "UnrealEnginePython" - if env.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): - - os.makedirs(uep_path, exist_ok=True) - dir_util._path_created = {} - dir_util.copy_tree( - env.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), - uep_path.as_posix()) - else: - # WARNING: this will trigger dev_mode, because we need to compile - # this plugin. - dev_mode = True - import git - git.Repo.clone_from( - "https://github.com/20tab/UnrealEnginePython.git", - uep_path.as_posix()) - - data["Plugins"].append( - {"Name": "UnrealEnginePython", "Enabled": True}) - - if not (uep_path / "Binaries").is_dir() \ - or not (uep_path / "Intermediate").is_dir(): - dev_mode = True - if dev_mode or preset["dev_mode"]: # this will add project module and necessary source file to make it # C++ project and to (hopefully) make Unreal Editor to compile all @@ -289,73 +271,31 @@ def create_unreal_project(project_name: str, "AdditionalDependencies": ["Engine"], }] - if preset["install_unreal_python_engine"]: - # now we need to fix python path in: - # `UnrealEnginePython.Build.cs` - # to point to our python - with open(uep_path / "Source" / "UnrealEnginePython" / - "UnrealEnginePython.Build.cs", mode="r") as f: - build_file = f.read() - - fix = build_file.replace( - 'private string pythonHome = "";', - 'private string pythonHome = "{}";'.format( - sys.base_prefix.replace("\\", "/"))) - - with open(uep_path / "Source" / "UnrealEnginePython" / - "UnrealEnginePython.Build.cs", mode="w") as f: - f.write(fix) - # write project file project_file = pr_dir / f"{project_name}.uproject" with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) - # ensure we have PySide installed in engine - # this won't work probably as pyside is no longer on pypi - # DEPRECATED: support for python 2 in UE4 is dropped. + # ensure we have PySide2 installed in engine python_path = None - if int(ue_version.split(".")[0]) == 4 and \ - int(ue_version.split(".")[1]) < 25: - if platform.system().lower() == "windows": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python/Win64/python.exe") + if platform.system().lower() == "windows": + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Win64/pythonw.exe") - if platform.system().lower() == "linux": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python/Linux/bin/python") + if platform.system().lower() == "linux": + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Linux/bin/python3") - if platform.system().lower() == "darwin": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python/Mac/bin/python") + if platform.system().lower() == "darwin": + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Mac/bin/python3") - if python_path.exists(): - subprocess.run([python_path.as_posix(), "-m", - "pip", "install", "pyside"]) - else: - raise NotImplementedError("Unsupported platform") - else: - # install PySide2 inside newer engines - if platform.system().lower() == "windows": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Win64/pythonw.exe") - - if platform.system().lower() == "linux": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Linux/bin/python3") - - if platform.system().lower() == "darwin": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Mac/bin/python3") - - if python_path: - if not python_path.exists(): - raise RuntimeError( - f"Unreal Python not found at {python_path}") - subprocess.run([python_path.as_posix(), "-m", - "pip", "install", "pyside2"]) - else: - raise NotImplementedError("Unsupported platform") + if not python_path: + raise NotImplementedError("Unsupported platform") + if not python_path.exists(): + raise RuntimeError(f"Unreal Python not found at {python_path}") + subprocess.run( + [python_path.as_posix(), "-m", "pip", "install", "pyside2"]) if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 2c4dd822bc..fcdec7a96c 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -33,6 +33,18 @@ class UnrealPrelaunchHook(PreLaunchHook): workdir = self.launch_context.env["AVALON_WORKDIR"] engine_version = self.app_name.split("/")[-1].replace("-", ".") unreal_project_name = f"{asset_name}_{task_name}" + try: + if int(engine_version.split(".")[0]) < 4 and \ + int(engine_version.split(".")[1]) < 26: + raise ApplicationLaunchFailed(( + f"{self.signature} Old unsupported version of UE4 " + f"detected - {engine_version}")) + except ValueError: + # there can be string in minor version and in that case + # int cast is failing. This probably happens only with + # early access versions and is of no concert for this check + # so lets keep it quite. + ... # Unreal is sensitive about project names longer then 20 chars if len(unreal_project_name) > 20: @@ -75,15 +87,8 @@ class UnrealPrelaunchHook(PreLaunchHook): f"detected [ {engine_version} ]" )) - ue4_path = Path(detected[engine_version]) / "Engine/Binaries" - if platform.system().lower() == "windows": - ue4_path = ue4_path / "Win64/UE4Editor.exe" - - elif platform.system().lower() == "linux": - ue4_path = ue4_path / "Linux/UE4Editor" - - elif platform.system().lower() == "darwin": - ue4_path = ue4_path / "Mac/UE4Editor" + ue4_path = unreal_lib.get_editor_executable_path( + Path(detected[engine_version])) self.launch_context.launch_args.append(ue4_path.as_posix()) project_path.mkdir(parents=True, exist_ok=True) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 72cd010cf2..fae89a36ca 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1095,7 +1095,7 @@ "unreal": { "enabled": true, "label": "Unreal Editor", - "icon": "{}/app_icons/ue4.png'", + "icon": "{}/app_icons/ue4.png", "host_name": "unreal", "environment": {}, "variants": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index b6e94d9d03..4e197e9fc8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -15,11 +15,6 @@ "type": "boolean", "key": "dev_mode", "label": "Dev mode" - }, - { - "type": "boolean", - "key": "install_unreal_python_engine", - "label": "Install unreal python engine" } ] } From f6700aadf3ec3b217d7775f3f7dcde2edb8f4820 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 29 Jun 2021 16:52:20 +0200 Subject: [PATCH 076/333] fix hound --- openpype/hosts/unreal/api/lib.py | 2 -- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 1 - 2 files changed, 3 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 144f781171..7e34c3ff15 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -230,8 +230,6 @@ def create_unreal_project(project_name: str, ue_id = "{" + loaded_modules.get("BuildId") + "}" plugins_path = None - uep_path = None - if os.path.isdir(env.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project plugins_path = pr_dir / "Plugins" diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index fcdec7a96c..01b8b6bc05 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -2,7 +2,6 @@ """Hook to launch Unreal and prepare projects.""" import os from pathlib import Path -import platform from openpype.lib import ( PreLaunchHook, From 672ee1d98db2a7adf341a63e635834ede87d139d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 16:57:01 +0200 Subject: [PATCH 077/333] store ignore_missing_defaults and reuse it on callbacks --- openpype/settings/entities/base_entity.py | 1 + openpype/settings/entities/dict_conditional.py | 1 + .../entities/dict_immutable_keys_entity.py | 1 + .../entities/dict_mutable_keys_entity.py | 18 ++++++++++++++---- openpype/settings/entities/input_entities.py | 1 + openpype/settings/entities/item_entities.py | 2 ++ openpype/settings/entities/list_entity.py | 18 ++++++++++++++---- 7 files changed, 34 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index e1cd5134e7..6c2f382403 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -136,6 +136,7 @@ class BaseItemEntity(BaseEntity): # Override state defines which values are used, saved and how. # TODO convert to private attribute self._override_state = OverrideState.NOT_DEFINED + self._ignore_missing_defaults = None # These attributes may change values during existence of an object # Default value, studio override values and project override values diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 0e6540e606..33cedd7b54 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -377,6 +377,7 @@ class DictConditionalEntity(ItemEntity): # Change has/had override states self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults # Set override state on enum entity first self.enum_entity.set_override_state(state, ignore_missing_defaults) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 2802290e68..bde5304787 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -266,6 +266,7 @@ class DictImmutableKeysEntity(ItemEntity): # Change has/had override states self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults for child_obj in self.non_gui_children.values(): child_obj.set_override_state(state, ignore_missing_defaults) diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index a5734e36b8..c3df935269 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -154,7 +154,9 @@ class DictMutableKeysEntity(EndpointEntity): def add_key(self, key): new_child = self._add_key(key) - new_child.set_override_state(self._override_state) + new_child.set_override_state( + self._override_state, self._ignore_missing_defaults + ) self.on_change() return new_child @@ -328,6 +330,8 @@ class DictMutableKeysEntity(EndpointEntity): # TODO change metadata self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults + # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: @@ -616,7 +620,9 @@ class DictMutableKeysEntity(EndpointEntity): if not self._can_discard_changes: return - self.set_override_state(self._override_state) + self.set_override_state( + self._override_state, self._ignore_missing_defaults + ) on_change_trigger.append(self.on_change) def _add_to_studio_default(self, _on_change_trigger): @@ -651,7 +657,9 @@ class DictMutableKeysEntity(EndpointEntity): if label: children_label_by_id[child_entity.id] = label - child_entity.set_override_state(self._override_state) + child_entity.set_override_state( + self._override_state, self._ignore_missing_defaults + ) self.children_label_by_id = children_label_by_id @@ -700,7 +708,9 @@ class DictMutableKeysEntity(EndpointEntity): if label: children_label_by_id[child_entity.id] = label - child_entity.set_override_state(self._override_state) + child_entity.set_override_state( + self._override_state, self._ignore_missing_defaults + ) self.children_label_by_id = children_label_by_id diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 9b41a26bdb..2abb7a2253 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -224,6 +224,7 @@ class InputEntity(EndpointEntity): return self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index c52eab988f..7e84f8c801 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -157,6 +157,7 @@ class PathEntity(ItemEntity): return self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults self.child_obj.set_override_state(state, ignore_missing_defaults) def update_default_value(self, value): @@ -351,6 +352,7 @@ class ListStrictEntity(ItemEntity): return self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index 2225523792..64bbad28a7 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -102,7 +102,9 @@ class ListEntity(EndpointEntity): def add_new_item(self, idx=None, trigger_change=True): child_obj = self._add_new_item(idx) - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, self._ignore_missing_defaults + ) if trigger_change: self.on_child_change(child_obj) @@ -212,6 +214,7 @@ class ListEntity(EndpointEntity): return self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults while self.children: self.children.pop(0) @@ -403,7 +406,9 @@ class ListEntity(EndpointEntity): if self.had_studio_override: child_obj.update_studio_value(item) - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, self._ignore_missing_defaults + ) if self._override_state >= OverrideState.PROJECT: self._has_project_override = self.had_project_override @@ -435,7 +440,9 @@ class ListEntity(EndpointEntity): for item in value: child_obj = self._add_new_item() child_obj.update_default_value(item) - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, self._ignore_missing_defaults + ) self._ignore_child_changes = False @@ -468,7 +475,10 @@ class ListEntity(EndpointEntity): child_obj.update_default_value(item) if self._has_studio_override: child_obj.update_studio_value(item) - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, + self._ignore_missing_defaults + ) self._ignore_child_changes = False From 477b4ecfcc8d421fd4334c87666d61767e049371 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 17:19:13 +0200 Subject: [PATCH 078/333] removed unused imports --- openpype/settings/entities/dict_conditional.py | 3 --- openpype/tools/settings/settings/dict_conditional.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 33cedd7b54..c115cac18a 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -1,8 +1,6 @@ import copy -import collections from .lib import ( - WRAPPER_TYPES, OverrideState, NOT_SET ) @@ -14,7 +12,6 @@ from openpype.settings.constants import ( from . import ( BaseItemEntity, ItemEntity, - BoolEntity, GUIEntity ) from .exceptions import ( diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 84288f7b5b..da2f53497e 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -1,5 +1,4 @@ -import collections -from Qt import QtWidgets, QtCore, QtGui +from Qt import QtWidgets from .widgets import ( ExpandingWidget, From ca7d91af622636b71adb4da42694b79ab8377409 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Jun 2021 13:53:15 +0200 Subject: [PATCH 079/333] fix typos --- openpype/settings/entities/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 42a08232b9..faaacd4230 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -147,7 +147,7 @@ class SchemasHub: crashed_item = self._crashed_on_load[schema_name] raise KeyError( "Unable to parse schema file \"{}\". {}".format( - crashed_item["filpath"], crashed_item["message"] + crashed_item["filepath"], crashed_item["message"] ) ) @@ -177,7 +177,7 @@ class SchemasHub: crashed_item = self._crashed_on_load[template_name] raise KeyError( "Unable to parse templace file \"{}\". {}".format( - crashed_item["filpath"], crashed_item["message"] + crashed_item["filepath"], crashed_item["message"] ) ) @@ -345,7 +345,7 @@ class SchemasHub: " One of them crashed on load \"{}\" {}" ).format( filename, - crashed_item["filpath"], + crashed_item["filepath"], crashed_item["message"] )) From 69a3d4f046bd53f1d2e84f762e63e4619ba11dc1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 1 Jul 2021 10:44:14 +0200 Subject: [PATCH 080/333] reraise exception if AVALON_APP is not set --- openpype/lib/editorial.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 158488dd56..8e8e365bdb 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -7,6 +7,8 @@ try: import opentimelineio as otio from opentimelineio import opentime as _ot except ImportError: + if not os.environ.get("AVALON_APP"): + raise otio = discover_host_vendor_module("opentimelineio") _ot = discover_host_vendor_module("opentimelineio.opentime") From 29ef07aada6ff9ee20f5b04ca25e99bcbb5b68c4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jul 2021 16:28:05 +0200 Subject: [PATCH 081/333] pr suggestion https://github.com/pypeclub/OpenPype/pull/1756#discussion_r660710802 --- .../deadline/plugins/publish/submit_nuke_deadline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 879c92490b..fed98d8a08 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -254,9 +254,9 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): if key in os.environ}, **api.Session) # self.log.debug("enviro: {}".format(pprint(environment))) - for path in os.environ: - if path.lower().startswith('openpype_'): - environment[path] = os.environ[path] + for _path in os.environ: + if _path.lower().startswith('openpype_'): + environment[_path] = os.environ[_path] clean_environment = {} for key, value in environment.items(): From ad1891e6375a8796b98e8082f7fe577db449877d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 14:55:35 +0200 Subject: [PATCH 082/333] add list version command, fix staging and use-version --- start.py | 90 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/start.py b/start.py index 8e7c195e95..7ce7d6afe7 100644 --- a/start.py +++ b/start.py @@ -135,18 +135,36 @@ if sys.__stdout__: def _print(message: str): if message.startswith("!!! "): print("{}{}".format(term.orangered2("!!! "), message[4:])) + return if message.startswith(">>> "): print("{}{}".format(term.aquamarine3(">>> "), message[4:])) + return if message.startswith("--- "): print("{}{}".format(term.darkolivegreen3("--- "), message[4:])) - if message.startswith(" "): - print("{}{}".format(term.darkseagreen3(" "), message[4:])) + return if message.startswith("*** "): print("{}{}".format(term.gold("*** "), message[4:])) + return if message.startswith(" - "): print("{}{}".format(term.wheat(" - "), message[4:])) + return if message.startswith(" . "): print("{}{}".format(term.tan(" . "), message[4:])) + return + if message.startswith(" - "): + print("{}{}".format(term.seagreen3(" - "), message[7:])) + return + if message.startswith(" ! "): + print("{}{}".format(term.goldenrod(" ! "), message[7:])) + return + if message.startswith(" * "): + print("{}{}".format(term.aquamarine1(" * "), message[7:])) + return + if message.startswith(" "): + print("{}{}".format(term.darkseagreen3(" "), message[4:])) + return + + print(message) else: def _print(message: str): print(message) @@ -175,6 +193,17 @@ silent_commands = ["run", "igniter", "standalonepublisher", "extractenvironments"] +def list_versions(openpype_versions: list, local_version=None) -> None: + """Print list of detected versions.""" + _print(" - Detected versions:") + for v in sorted(openpype_versions): + _print(f" - {v}: {v.path}") + if not openpype_versions: + _print(" ! none in repository detected") + if local_version: + _print(f" * local version {local_version}") + + def set_openpype_global_environments() -> None: """Set global OpenPype's environments.""" import acre @@ -303,6 +332,7 @@ def _process_arguments() -> tuple: # check for `--use-version=3.0.0` argument and `--use-staging` use_version = None use_staging = False + print_versions = False for arg in sys.argv: if arg == "--use-version": _print("!!! Please use option --use-version like:") @@ -313,12 +343,19 @@ def _process_arguments() -> tuple: r"--use-version=(?P\d+\.\d+\.\d+(?:\S*)?)", arg) if m and m.group('version'): use_version = m.group('version') + _print(">>> Requested version [ {} ]".format(use_version)) sys.argv.remove(arg) + if "+staging" in use_version: + use_staging = True break if "--use-staging" in sys.argv: use_staging = True sys.argv.remove("--use-staging") + if "--list-versions" in sys.argv: + print_versions = True + sys.argv.remove("--list-versions") + # handle igniter # this is helper to run igniter before anything else if "igniter" in sys.argv: @@ -334,7 +371,7 @@ def _process_arguments() -> tuple: sys.argv.pop(idx) sys.argv.insert(idx, "tray") - return use_version, use_staging + return use_version, use_staging, print_versions def _determine_mongodb() -> str: @@ -487,7 +524,7 @@ def _find_frozen_openpype(use_version: str = None, openpype_version = openpype_versions[-1] except IndexError: _print(("!!! Something is wrong and we didn't " - "found it again.")) + "found it again.")) sys.exit(1) elif return_code != 2: _print(f" . finished ({return_code})") @@ -519,13 +556,8 @@ def _find_frozen_openpype(use_version: str = None, if found: openpype_version = sorted(found)[-1] if not openpype_version: - _print(f"!!! requested version {use_version} was not found.") - if openpype_versions: - _print(" - found: ") - for v in sorted(openpype_versions): - _print(f" - {v}: {v.path}") - - _print(f" - local version {local_version}") + _print(f"!!! Requested version {use_version} was not found.") + list_versions(openpype_versions, local_version) sys.exit(1) # test if latest detected is installed (in user data dir) @@ -560,7 +592,7 @@ def _find_frozen_openpype(use_version: str = None, return openpype_version.path -def _bootstrap_from_code(use_version): +def _bootstrap_from_code(use_version, use_staging): """Bootstrap live code (or the one coming with frozen OpenPype. Args: @@ -583,7 +615,8 @@ def _bootstrap_from_code(use_version): if use_version and use_version != local_version: version_to_use = None - openpype_versions = bootstrap.find_openpype(include_zips=True) + openpype_versions = bootstrap.find_openpype( + include_zips=True, staging=use_staging) v: OpenPypeVersion found = [v for v in openpype_versions if str(v) == use_version] if found: @@ -600,13 +633,8 @@ def _bootstrap_from_code(use_version): os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501 _openpype_root = version_to_use.path.as_posix() else: - _print(f"!!! requested version {use_version} was not found.") - if openpype_versions: - _print(" - found: ") - for v in sorted(openpype_versions): - _print(f" - {v}: {v.path}") - - _print(f" - local version {local_version}") + _print(f"!!! Requested version {use_version} was not found.") + list_versions(openpype_versions, local_version) sys.exit(1) else: os.environ["OPENPYPE_VERSION"] = local_version @@ -675,7 +703,7 @@ def boot(): # Process arguments # ------------------------------------------------------------------------ - use_version, use_staging = _process_arguments() + use_version, use_staging, print_versions = _process_arguments() if os.getenv("OPENPYPE_VERSION"): use_staging = "staging" in os.getenv("OPENPYPE_VERSION") @@ -704,6 +732,24 @@ def boot(): if not os.getenv("OPENPYPE_PATH") and openpype_path: os.environ["OPENPYPE_PATH"] = openpype_path + if print_versions: + if not use_staging: + _print("--- This will list only non-staging versions detected.") + _print(" To see staging versions, use --use-staging argument.") + else: + _print("--- This will list only staging versions detected.") + _print(" To see other version, omit --use-staging argument.") + _openpype_root = OPENPYPE_ROOT + openpype_versions = bootstrap.find_openpype(include_zips=True, + staging=use_staging) + if getattr(sys, 'frozen', False): + local_version = bootstrap.get_version(Path(_openpype_root)) + else: + local_version = bootstrap.get_local_live_version() + + list_versions(openpype_versions, local_version) + sys.exit(1) + # ------------------------------------------------------------------------ # Find OpenPype versions # ------------------------------------------------------------------------ @@ -718,7 +764,7 @@ def boot(): _print(f"!!! {e}") sys.exit(1) else: - version_path = _bootstrap_from_code(use_version) + version_path = _bootstrap_from_code(use_version, use_staging) # set this to point either to `python` from venv in case of live code # or to `openpype` or `openpype_console` in case of frozen code From 8c342f5bd65dde388ed513030b7c49c76ddc6c43 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 14:56:22 +0200 Subject: [PATCH 083/333] fix version resolution --- igniter/bootstrap_repos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 6eaea27116..8c081b8614 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -657,7 +657,7 @@ class BootstrapRepos: ] # remove duplicates - openpype_versions = list(set(openpype_versions)) + openpype_versions = sorted(list(set(openpype_versions))) return openpype_versions From 920b94fa7f4a6cdb8ecdf40751a6e765478a227d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 14:56:41 +0200 Subject: [PATCH 084/333] add more info to tests --- tests/igniter/test_bootstrap_repos.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/tests/igniter/test_bootstrap_repos.py b/tests/igniter/test_bootstrap_repos.py index 743131acfa..740a71a5ce 100644 --- a/tests/igniter/test_bootstrap_repos.py +++ b/tests/igniter/test_bootstrap_repos.py @@ -330,8 +330,8 @@ def test_find_openpype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): assert result[-1].path == expected_path, ("not a latest version of " "OpenPype 3") + printer("testing finding OpenPype in OPENPYPE_PATH ...") monkeypatch.setenv("OPENPYPE_PATH", e_path.as_posix()) - result = fix_bootstrap.find_openpype(include_zips=True) # we should have results as file were created assert result is not None, "no OpenPype version found" @@ -349,6 +349,8 @@ def test_find_openpype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): monkeypatch.delenv("OPENPYPE_PATH", raising=False) + printer("testing finding OpenPype in user data dir ...") + # mock appdirs user_data_dir def mock_user_data_dir(*args, **kwargs): """Mock local app data dir.""" @@ -373,18 +375,7 @@ def test_find_openpype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): assert result[-1].path == expected_path, ("not a latest version of " "OpenPype 2") - result = fix_bootstrap.find_openpype(e_path, include_zips=True) - assert result is not None, "no OpenPype version found" - expected_path = Path( - e_path / "{}{}{}".format( - test_versions_1[5].prefix, - test_versions_1[5].version, - test_versions_1[5].suffix - ) - ) - assert result[-1].path == expected_path, ("not a latest version of " - "OpenPype 1") - + printer("testing finding OpenPype zip/dir precedence ...") result = fix_bootstrap.find_openpype(dir_path, include_zips=True) assert result is not None, "no OpenPype versions found" expected_path = Path( From 8c034a9a68d2b1f6f1e5453a28c0be50523fcbe6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 15:04:17 +0200 Subject: [PATCH 085/333] add documentation to --list-versions --- website/docs/admin_openpype_commands.md | 2 ++ website/docs/admin_use.md | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 324e0e8481..1a91e2e7fe 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -21,6 +21,8 @@ openpype_console --use-version=3.0.0-foo+bar `--use-staging` - to use staging versions of OpenPype. +`--list-versions [--use-staging]` - to list available versions. + For more information [see here](admin_use#run-openpype). ## Commands diff --git a/website/docs/admin_use.md b/website/docs/admin_use.md index 4a2b56e6f4..4ad08a0174 100644 --- a/website/docs/admin_use.md +++ b/website/docs/admin_use.md @@ -46,6 +46,16 @@ openpype_console --use-version=3.0.1 `--use-staging` - to specify you prefer staging version. In that case it will be used (if found) instead of production one. +:::tip List available versions +To list all available versions, use: + +```shell +openpype_console --list-versions +``` + +You can add `--use-staging` to list staging versions. +::: + ### Details When you run OpenPype from executable, few check are made: From 07a18a15b2207f46dabf98eb0fc5ab4f095ca081 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 16:08:10 +0200 Subject: [PATCH 086/333] fix argument --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 7ce7d6afe7..cd6be0a7dd 100644 --- a/start.py +++ b/start.py @@ -537,7 +537,7 @@ def _find_frozen_openpype(use_version: str = None, _print("*** Still no luck finding OpenPype.") _print(("*** We'll try to use the one coming " "with OpenPype installation.")) - version_path = _bootstrap_from_code(use_version) + version_path = _bootstrap_from_code(use_version, use_staging) openpype_version = OpenPypeVersion( version=BootstrapRepos.get_version(version_path), path=version_path) From ad3acb706c0522888bd3d57926da59026a225bd7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 16:45:47 +0200 Subject: [PATCH 087/333] always create log file during build --- tools/build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index c8c2f392ad..89795b0a50 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -185,9 +185,9 @@ Write-Host "Building OpenPype ..." $startTime = [int][double]::Parse((Get-Date -UFormat %s)) $out = & poetry run python setup.py build 2>&1 +Set-Content -Path "$($openpype_root)\build\build.log" -Value $out if ($LASTEXITCODE -ne 0) { - Set-Content -Path "$($openpype_root)\build\build.log" -Value $out Write-Host "!!! " -NoNewLine -ForegroundColor Red Write-Host "Build failed. Check the log: " -NoNewline Write-Host ".\build\build.log" -ForegroundColor Yellow From c2b6f199c61a4e66e0cdd963a80d6967038eca4f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 16:46:52 +0200 Subject: [PATCH 088/333] fail if invalid version format requested --- start.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/start.py b/start.py index cd6be0a7dd..b2fff4eac0 100644 --- a/start.py +++ b/start.py @@ -348,6 +348,12 @@ def _process_arguments() -> tuple: if "+staging" in use_version: use_staging = True break + else: + _print("!!! Requested version isn't in correct format.") + _print((" Use --list-versions to find out" + " proper version string.")) + sys.exit(1) + if "--use-staging" in sys.argv: use_staging = True sys.argv.remove("--use-staging") @@ -607,7 +613,8 @@ def _bootstrap_from_code(use_version, use_staging): _openpype_root = OPENPYPE_ROOT if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(_openpype_root)) - _print(f" - running version: {local_version}") + switch_str = f" - will switch to {use_version}" if use_version else "" + _print(f" - booting version: {local_version}{switch_str}") assert local_version else: # get current version of OpenPype @@ -706,8 +713,13 @@ def boot(): use_version, use_staging, print_versions = _process_arguments() if os.getenv("OPENPYPE_VERSION"): - use_staging = "staging" in os.getenv("OPENPYPE_VERSION") - use_version = os.getenv("OPENPYPE_VERSION") + if use_version: + _print(("*** environment variable OPENPYPE_VERSION" + "is overridden by command line argument.")) + else: + _print(">>> version set by environment variable") + use_staging = "staging" in os.getenv("OPENPYPE_VERSION") + use_version = os.getenv("OPENPYPE_VERSION") # ------------------------------------------------------------------------ # Determine mongodb connection From 3b71660224f440e280671d4d4d43ef0b79a7ecbd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 16:49:20 +0200 Subject: [PATCH 089/333] fix staging info --- start.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/start.py b/start.py index b2fff4eac0..0b8528bade 100644 --- a/start.py +++ b/start.py @@ -812,7 +812,7 @@ def boot(): from openpype.version import __version__ assert version_path, "Version path not defined." - info = get_info() + info = get_info(use_staging) info.insert(0, f">>> Using OpenPype from [ {version_path} ]") t_width = 20 @@ -839,7 +839,7 @@ def boot(): sys.exit(1) -def get_info() -> list: +def get_info(use_staging=None) -> list: """Print additional information to console.""" from openpype.lib.mongo import get_default_components from openpype.lib.log import PypeLogger @@ -847,7 +847,7 @@ def get_info() -> list: components = get_default_components() inf = [] - if not getattr(sys, 'frozen', False): + if use_staging: inf.append(("OpenPype variant", "staging")) else: inf.append(("OpenPype variant", "production")) From bbd6e1f24e6b9f0cd52823408f01b9ccbd26e433 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 17:02:55 +0200 Subject: [PATCH 090/333] fix processing of `--use-version` --- start.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/start.py b/start.py index 0b8528bade..44f8e1b0da 100644 --- a/start.py +++ b/start.py @@ -339,20 +339,21 @@ def _process_arguments() -> tuple: _print(" --use-version=3.0.0") sys.exit(1) - m = re.search( - r"--use-version=(?P\d+\.\d+\.\d+(?:\S*)?)", arg) - if m and m.group('version'): - use_version = m.group('version') - _print(">>> Requested version [ {} ]".format(use_version)) - sys.argv.remove(arg) - if "+staging" in use_version: - use_staging = True - break - else: - _print("!!! Requested version isn't in correct format.") - _print((" Use --list-versions to find out" - " proper version string.")) - sys.exit(1) + if arg.startswith("--use-version="): + m = re.search( + r"--use-version=(?P\d+\.\d+\.\d+(?:\S*)?)", arg) + if m and m.group('version'): + use_version = m.group('version') + _print(">>> Requested version [ {} ]".format(use_version)) + sys.argv.remove(arg) + if "+staging" in use_version: + use_staging = True + break + else: + _print("!!! Requested version isn't in correct format.") + _print((" Use --list-versions to find out" + " proper version string.")) + sys.exit(1) if "--use-staging" in sys.argv: use_staging = True From 3ead357d79d029371d7e368bea633e6af0a80a36 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 3 Jul 2021 03:41:04 +0000 Subject: [PATCH 091/333] [Automated] Bump version --- CHANGELOG.md | 16 ++++++++++++---- openpype/version.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b69a8e2d5..4e658d6995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,18 @@ # Changelog -## [3.2.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776) - Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769) - Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766) - Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) +- Deadline: Nuke submission additional attributes [\#1756](https://github.com/pypeclub/OpenPype/pull/1756) +- Settings schema without prefill [\#1753](https://github.com/pypeclub/OpenPype/pull/1753) - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) @@ -22,7 +25,9 @@ **🐛 Bug fixes** +- Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) - FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775) +- Fix - single file files are str only, cast it to list to count properly [\#1772](https://github.com/pypeclub/OpenPype/pull/1772) - Project specific environments [\#1767](https://github.com/pypeclub/OpenPype/pull/1767) - Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764) - Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761) @@ -46,7 +51,7 @@ - Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) - TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) -- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) +- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) @@ -61,6 +66,10 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) +**🚀 Enhancements** + +- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) + **🐛 Bug fixes** - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) @@ -88,7 +97,6 @@ **🚀 Enhancements** -- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) - OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) - Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) @@ -116,10 +124,10 @@ - Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) - Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) -- New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) **Merged pull requests:** +- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) diff --git a/openpype/version.py b/openpype/version.py index 0371d5f4e3..86d62a83d0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.5" +__version__ = "3.2.0-nightly.6" From ec0a57f87667d583b55d826e9b1a06b6c94f0c68 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 10:41:37 +0200 Subject: [PATCH 092/333] make sure email is not None but string --- .../event_handlers_server/action_clone_review_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py index d29316c795..e165466d00 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py +++ b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py @@ -16,11 +16,12 @@ def clone_review_session(session, entity): # Add all invitees. for invitee in entity["review_session_invitees"]: + email = invitee["email"] or "" session.create( "ReviewSessionInvitee", { "name": invitee["name"], - "email": invitee["email"], + "email": email, "review_session": review_session } ) From 3d086c967a0dd79c5c320bf4a552e2de8dab9425 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 10:46:11 +0200 Subject: [PATCH 093/333] added comment --- .../ftrack/event_handlers_server/action_clone_review_session.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py index e165466d00..59c8bffb75 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py +++ b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py @@ -16,6 +16,7 @@ def clone_review_session(session, entity): # Add all invitees. for invitee in entity["review_session_invitees"]: + # Make sure email is not None but string email = invitee["email"] or "" session.create( "ReviewSessionInvitee", From 11da3d0d4dc26992e8dd2fcd4344d46aa19a57f6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Jul 2021 12:12:11 +0200 Subject: [PATCH 094/333] add `--list-versions` help --- openpype/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/cli.py b/openpype/cli.py index 48951c7287..ec5b04c468 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -15,6 +15,9 @@ from .pype_commands import PypeCommands expose_value=False, help="use specified version") @click.option("--use-staging", is_flag=True, expose_value=False, help="use staging variants") +@click.option("--list-versions", is_flag=True, expose_value=False, + help=("list all detected versions. Use With `--use-staging " + "to list staging versions.")) def main(ctx): """Pype is main command serving as entry point to pipeline system. From 9d8b3d222442200b15f770c01ee80ef43c80f468 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Jul 2021 12:46:32 +0200 Subject: [PATCH 095/333] fix standalone `--use-staging` --- start.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/start.py b/start.py index 44f8e1b0da..1b5c25ae3a 100644 --- a/start.py +++ b/start.py @@ -621,10 +621,26 @@ def _bootstrap_from_code(use_version, use_staging): # get current version of OpenPype local_version = bootstrap.get_local_live_version() - if use_version and use_version != local_version: - version_to_use = None - openpype_versions = bootstrap.find_openpype( - include_zips=True, staging=use_staging) + version_to_use = None + openpype_versions = bootstrap.find_openpype( + include_zips=True, staging=use_staging) + if use_staging and not use_version: + try: + version_to_use = openpype_versions[-1] + except IndexError: + _print("!!! No staging versions are found.") + list_versions(openpype_versions, local_version) + sys.exit(1) + if version_to_use.path.is_file(): + version_to_use.path = bootstrap.extract_openpype( + version_to_use) + bootstrap.add_paths_from_directory(version_to_use.path) + os.environ["OPENPYPE_VERSION"] = str(version_to_use) + version_path = version_to_use.path + os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501 + _openpype_root = version_to_use.path.as_posix() + + elif use_version and use_version != local_version: v: OpenPypeVersion found = [v for v in openpype_versions if str(v) == use_version] if found: From f0e6bb7a82a98e040f47087bdb00fc7ffed47cbc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:42:30 +0200 Subject: [PATCH 096/333] created copy of multiple notes action --- .../action_multiple_notes.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py new file mode 100644 index 0000000000..8db65fe39b --- /dev/null +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -0,0 +1,110 @@ +from openpype.modules.ftrack.lib import BaseAction, statics_icon + + +class MultipleNotes(BaseAction): + '''Edit meta data action.''' + + #: Action identifier. + identifier = 'multiple.notes' + #: Action label. + label = 'Multiple Notes' + #: Action description. + description = 'Add same note to multiple Asset Versions' + icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg") + + def discover(self, session, entities, event): + ''' Validation ''' + valid = True + for entity in entities: + if entity.entity_type.lower() != 'assetversion': + valid = False + break + return valid + + def interface(self, session, entities, event): + if not event['data'].get('values', {}): + note_label = { + 'type': 'label', + 'value': '# Enter note: #' + } + + note_value = { + 'name': 'note', + 'type': 'textarea' + } + + category_label = { + 'type': 'label', + 'value': '## Category: ##' + } + + category_data = [] + category_data.append({ + 'label': '- None -', + 'value': 'none' + }) + all_categories = session.query('NoteCategory').all() + for cat in all_categories: + category_data.append({ + 'label': cat['name'], + 'value': cat['id'] + }) + category_value = { + 'type': 'enumerator', + 'name': 'category', + 'data': category_data, + 'value': 'none' + } + + splitter = { + 'type': 'label', + 'value': '{}'.format(200*"-") + } + + items = [] + items.append(note_label) + items.append(note_value) + items.append(splitter) + items.append(category_label) + items.append(category_value) + return items + + def launch(self, session, entities, event): + if 'values' not in event['data']: + return + + values = event['data']['values'] + if len(values) <= 0 or 'note' not in values: + return False + # Get Note text + note_value = values['note'] + if note_value.lower().strip() == '': + return False + # Get User + user = session.query( + 'User where username is "{}"'.format(session.api_user) + ).one() + # Base note data + note_data = { + 'content': note_value, + 'author': user + } + # Get category + category_value = values['category'] + if category_value != 'none': + category = session.query( + 'NoteCategory where id is "{}"'.format(category_value) + ).one() + note_data['category'] = category + # Create notes for entities + for entity in entities: + new_note = session.create('Note', note_data) + entity['notes'].append(new_note) + session.commit() + return True + + +def register(session): + '''Register plugin. Called when used as an plugin.''' + + MultipleNotes(session).register() From 4a2efb1cd2ae9b617ceaf8ddd45eb1b304e7de2f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:53:06 +0200 Subject: [PATCH 097/333] Converted to server action --- .../action_multiple_notes.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 8db65fe39b..021a61e0ce 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -1,16 +1,12 @@ -from openpype.modules.ftrack.lib import BaseAction, statics_icon +from openpype.modules.ftrack.lib import ServerAction -class MultipleNotes(BaseAction): - '''Edit meta data action.''' +class MultipleNotesServer(ServerAction): + """Action adds same note for muliple AssetVersions.""" - #: Action identifier. - identifier = 'multiple.notes' - #: Action label. - label = 'Multiple Notes' - #: Action description. - description = 'Add same note to multiple Asset Versions' - icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg") + identifier = "multiple.notes.server" + label = "Multiple Notes (Server)" + description = "Add same note to multiple Asset Versions" def discover(self, session, entities, event): ''' Validation ''' @@ -107,4 +103,4 @@ class MultipleNotes(BaseAction): def register(session): '''Register plugin. Called when used as an plugin.''' - MultipleNotes(session).register() + MultipleNotesServer(session).register() From c1994950adb7cc28704f409c3486b5a875d36311 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:54:07 +0200 Subject: [PATCH 098/333] use user from event instead of session event --- .../action_multiple_notes.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 021a61e0ce..c41b900031 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -18,6 +18,15 @@ class MultipleNotesServer(ServerAction): return valid def interface(self, session, entities, event): + event_source = event["source"] + user_info = event_source.get("user") or {} + user_id = user_info.get("id") + if not user_id: + return { + "success": False, + "message": "Couldn't get user information." + } + if not event['data'].get('values', {}): note_label = { 'type': 'label', @@ -77,9 +86,21 @@ class MultipleNotesServer(ServerAction): if note_value.lower().strip() == '': return False # Get User - user = session.query( - 'User where username is "{}"'.format(session.api_user) - ).one() + event_source = event["source"] + user_info = event_source.get("user") or {} + user_id = user_info.get("id") + user = None + if user_id: + user = session.query( + 'User where id is "{}"'.format(user_id) + ).first() + + if not user: + return { + "success": False, + "message": "Couldn't get user information." + } + # Base note data note_data = { 'content': note_value, From 2710bce4bd1d69ef20c8867db8db13f3dfb2f0be Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:57:46 +0200 Subject: [PATCH 099/333] use constant for none category --- .../ftrack/event_handlers_server/action_multiple_notes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index c41b900031..64b5161366 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -8,6 +8,8 @@ class MultipleNotesServer(ServerAction): label = "Multiple Notes (Server)" description = "Add same note to multiple Asset Versions" + _none_category = "__NONE__" + def discover(self, session, entities, event): ''' Validation ''' valid = True @@ -46,7 +48,7 @@ class MultipleNotesServer(ServerAction): category_data = [] category_data.append({ 'label': '- None -', - 'value': 'none' + "value": self._none_category }) all_categories = session.query('NoteCategory').all() for cat in all_categories: @@ -58,7 +60,7 @@ class MultipleNotesServer(ServerAction): 'type': 'enumerator', 'name': 'category', 'data': category_data, - 'value': 'none' + "value": self._none_category } splitter = { @@ -108,7 +110,7 @@ class MultipleNotesServer(ServerAction): } # Get category category_value = values['category'] - if category_value != 'none': + if category_value != self._none_category: category = session.query( 'NoteCategory where id is "{}"'.format(category_value) ).one() From 61588d9ddd317213dd0795c69b8fd8f22ee898ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:58:56 +0200 Subject: [PATCH 100/333] formatting changes --- .../action_multiple_notes.py | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 64b5161366..d08e66ff56 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -14,7 +14,7 @@ class MultipleNotesServer(ServerAction): ''' Validation ''' valid = True for entity in entities: - if entity.entity_type.lower() != 'assetversion': + if entity.entity_type.lower() != "assetversion": valid = False break return valid @@ -31,62 +31,64 @@ class MultipleNotesServer(ServerAction): if not event['data'].get('values', {}): note_label = { - 'type': 'label', - 'value': '# Enter note: #' + "type": "label", + "value": "# Enter note: #" } note_value = { - 'name': 'note', - 'type': 'textarea' + "name": "note", + "type": "textarea" } category_label = { - 'type': 'label', - 'value': '## Category: ##' + "type": "label", + "value": "## Category: ##" } category_data = [] category_data.append({ - 'label': '- None -', + "label": "- None -", "value": self._none_category }) all_categories = session.query('NoteCategory').all() for cat in all_categories: category_data.append({ - 'label': cat['name'], - 'value': cat['id'] + "label": cat["name"], + "value": cat["id"] }) category_value = { - 'type': 'enumerator', - 'name': 'category', - 'data': category_data, + "type": "enumerator", + "name": "category", + "data": category_data, "value": self._none_category } splitter = { - 'type': 'label', - 'value': '{}'.format(200*"-") + "type": "label", + "value": "---" } - items = [] - items.append(note_label) - items.append(note_value) - items.append(splitter) - items.append(category_label) - items.append(category_value) - return items + return [ + note_label, + note_value, + splitter, + category_label, + category_value + ] def launch(self, session, entities, event): - if 'values' not in event['data']: + if "values" not in event["data"]: return - values = event['data']['values'] - if len(values) <= 0 or 'note' not in values: + values = event["data"]["values"] + if len(values) <= 0 or "note" not in values: return False + # Get Note text - note_value = values['note'] - if note_value.lower().strip() == '': + note_value = values["note"] + if note_value.lower().strip() == "": return False + # Get User event_source = event["source"] user_info = event_source.get("user") or {} @@ -105,20 +107,20 @@ class MultipleNotesServer(ServerAction): # Base note data note_data = { - 'content': note_value, - 'author': user + "content": note_value, + "author": user } # Get category - category_value = values['category'] + category_value = values["category"] if category_value != self._none_category: category = session.query( - 'NoteCategory where id is "{}"'.format(category_value) + "NoteCategory where id is \"{}\"".format(category_value) ).one() - note_data['category'] = category + note_data["category"] = category # Create notes for entities for entity in entities: - new_note = session.create('Note', note_data) - entity['notes'].append(new_note) + new_note = session.create("Note", note_data) + entity["notes"].append(new_note) session.commit() return True From 52556fc9cea02ce331ba59ef556ccf6bb2f7dbaf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:59:28 +0200 Subject: [PATCH 101/333] reversed logic of interface --- .../action_multiple_notes.py | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index d08e66ff56..7b8e883174 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -29,52 +29,57 @@ class MultipleNotesServer(ServerAction): "message": "Couldn't get user information." } - if not event['data'].get('values', {}): - note_label = { - "type": "label", - "value": "# Enter note: #" - } + values = event["data"].get("values") + if values: + return None - note_value = { - "name": "note", - "type": "textarea" - } + note_label = { + "type": "label", + "value": "# Enter note: #" + } - category_label = { - "type": "label", - "value": "## Category: ##" - } + note_value = { + "name": "note", + "type": "textarea" + } - category_data = [] + category_label = { + "type": "label", + "value": "## Category: ##" + } + + category_data = [] + category_data.append({ + "label": "- None -", + "value": self._none_category + }) + all_categories = session.query( + "select id, name from NoteCategory" + ).all() + for cat in all_categories: category_data.append({ - "label": "- None -", - "value": self._none_category + "label": cat["name"], + "value": cat["id"] }) - all_categories = session.query('NoteCategory').all() - for cat in all_categories: - category_data.append({ - "label": cat["name"], - "value": cat["id"] - }) - category_value = { - "type": "enumerator", - "name": "category", - "data": category_data, - "value": self._none_category - } + category_value = { + "type": "enumerator", + "name": "category", + "data": category_data, + "value": self._none_category + } - splitter = { - "type": "label", - "value": "---" - } + splitter = { + "type": "label", + "value": "---" + } - return [ - note_label, - note_value, - splitter, - category_label, - category_value - ] + return [ + note_label, + note_value, + splitter, + category_label, + category_value + ] def launch(self, session, entities, event): if "values" not in event["data"]: From 17a97044b9bef033aefc392f7a3aa8011de0be6f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 18:23:21 +0200 Subject: [PATCH 102/333] addedd few modifications to discovery --- .../event_handlers_server/action_multiple_notes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 7b8e883174..7ed0129951 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -11,13 +11,14 @@ class MultipleNotesServer(ServerAction): _none_category = "__NONE__" def discover(self, session, entities, event): - ''' Validation ''' - valid = True + """Show action only on AssetVersions.""" + if not entities: + return False + for entity in entities: if entity.entity_type.lower() != "assetversion": - valid = False - break - return valid + return False + return True def interface(self, session, entities, event): event_source = event["source"] From baf02590c24c27961ce60657f4901d894c538a06 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 18:23:50 +0200 Subject: [PATCH 103/333] fixed few return values --- .../event_handlers_server/action_multiple_notes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 7ed0129951..1c3c9929d9 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -25,10 +25,7 @@ class MultipleNotesServer(ServerAction): user_info = event_source.get("user") or {} user_id = user_info.get("id") if not user_id: - return { - "success": False, - "message": "Couldn't get user information." - } + return None values = event["data"].get("values") if values: @@ -84,7 +81,7 @@ class MultipleNotesServer(ServerAction): def launch(self, session, entities, event): if "values" not in event["data"]: - return + return None values = event["data"]["values"] if len(values) <= 0 or "note" not in values: @@ -93,7 +90,10 @@ class MultipleNotesServer(ServerAction): # Get Note text note_value = values["note"] if note_value.lower().strip() == "": - return False + return { + "success": True, + "message": "Note was not entered. Skipping" + } # Get User event_source = event["source"] From cd3cd88dfacef90a8b0258291ad907d5f7f7ed90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 18:25:07 +0200 Subject: [PATCH 104/333] added debug log --- .../action_multiple_notes.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 1c3c9929d9..340dd659af 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -111,18 +111,44 @@ class MultipleNotesServer(ServerAction): "message": "Couldn't get user information." } + # Logging message preparation + # - username + username = user.get("username") or "N/A" + + # - AssetVersion ids + asset_version_ids_str = ",".join([entity["id"] for entity in entities]) + # Base note data note_data = { "content": note_value, "author": user } + # Get category - category_value = values["category"] - if category_value != self._none_category: + category_id = values["category"] + if category_id == self._none_category: + category_id = None + + category_name = None + if category_id is not None: category = session.query( - "NoteCategory where id is \"{}\"".format(category_value) - ).one() - note_data["category"] = category + "select id, name from NoteCategory where id is \"{}\"".format( + category_id + ) + ).first() + if category: + note_data["category"] = category + category_name = category["name"] + + category_msg = "" + if category_name: + category_msg = " with category: \"{}\"".format(category_name) + + self.log.warning(( + "Creating note{} as User \"{}\" on " + "AssetVersions: {} with value \"{}\"" + ).format(category_msg, username, asset_version_ids_str, note_value)) + # Create notes for entities for entity in entities: new_note = session.create("Note", note_data) From 2be9dd55ee5a52bc0efb7ec46a87d721bd0d9ee0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 18:25:14 +0200 Subject: [PATCH 105/333] enhanced docstring --- .../ftrack/event_handlers_server/action_multiple_notes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 340dd659af..9ad7b1a969 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -2,7 +2,11 @@ from openpype.modules.ftrack.lib import ServerAction class MultipleNotesServer(ServerAction): - """Action adds same note for muliple AssetVersions.""" + """Action adds same note for muliple AssetVersions. + + Note is added to selection of AssetVersions. Note is created with user + who triggered the action. It is possible to define note category of note. + """ identifier = "multiple.notes.server" label = "Multiple Notes (Server)" From afdb5a8f59d252d0f9f6938896820472ad15b891 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Jul 2021 19:17:34 +0200 Subject: [PATCH 106/333] add pluginInfo and jobInfo custom settings --- .../plugins/publish/submit_maya_deadline.py | 19 +++++++++++++++++++ .../defaults/project_settings/maya.json | 4 ++++ .../schemas/schema_maya_publish.json | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a5841f406c..0e09641200 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -271,6 +271,21 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): ["DEADLINE_REST_URL"] ) + self._job_info = ( + context.data["project_settings"] + ["maya"] + ["publish"] + ["deadline"] + ["jobInfo"] + ) + self._plugin_info = ( + context.data["project_settings"] + ["maya"] + ["publish"] + ["deadline"] + ["pluginInfo"] + ) + assert self._deadline_url, "Requires DEADLINE_REST_URL" context = instance.context @@ -536,6 +551,10 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.preflight_check(instance) + # add jobInfo and pluginInfo variables from Settings + payload["JobInfo"].update(self._job_info) + payload["PluginInfo"].update(self._plugin_info) + # Prepare tiles data ------------------------------------------------ if instance.data.get("tileRendering"): # if we have sequence of files, we need to create tile job for diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 284a1a0040..62d8a74670 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -391,6 +391,10 @@ } } }, + "deadline": { + "jobInfo": {}, + "pluginInfo": {} + }, "ExtractCameraAlembic": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 5ca7059ee5..7e50682f5d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -309,6 +309,24 @@ "type": "schema", "name": "schema_maya_capture" }, + { + "type": "dict", + "collapsible": true, + "key": "deadline", + "label": "Additional Deadline Settings", + "children": [ + { + "type": "raw-json", + "key": "jobInfo", + "label": "Additional JobInfo data" + }, + { + "type": "raw-json", + "key": "pluginInfo", + "label": "Additional PluginInfo data" + } + ] + }, { "type": "dict", "collapsible": true, From 898157c95be9205d77934a03d5c02a7b658e3978 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:28:12 +0200 Subject: [PATCH 107/333] fixed typo --- openpype/settings/entities/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index faaacd4230..e58281644a 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -176,7 +176,7 @@ class SchemasHub: elif template_name in self._crashed_on_load: crashed_item = self._crashed_on_load[template_name] raise KeyError( - "Unable to parse templace file \"{}\". {}".format( + "Unable to parse template file \"{}\". {}".format( crashed_item["filepath"], crashed_item["message"] ) ) From ec01e148e56cc17b3b50dfe811e5cf27d4be07ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:36:32 +0200 Subject: [PATCH 108/333] added missing attributes --- openpype/settings/entities/dict_conditional.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index c115cac18a..641986491c 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -144,6 +144,11 @@ class DictConditionalEntity(ItemEntity): self.enum_entity = None + self.highlight_content = self.schema_data.get( + "highlight_content", False + ) + self.show_borders = self.schema_data.get("show_borders", True) + self._add_children() @property From 1cb8d0f5e8b64f9d6990deebb103ddbf920eb987 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:37:43 +0200 Subject: [PATCH 109/333] added example of conditional dictionary --- .../schemas/system_schema/example_schema.json | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index a4ed56df32..c3287d7452 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -9,6 +9,54 @@ "label": "Color input", "type": "color" }, + { + "type": "dict-conditional", + "use_label_wrap": true, + "collapsible": true, + "key": "menu_items", + "label": "Menu items", + "enum_key": "type", + "enum_label": "Type", + "enum_children": [ + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "command", + "label": "Comand" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "key": "children", + "label": "Children", + "type": "list", + "object_type": "text" + } + ] + }, + { + "key": "separator", + "label": "Separator" + } + ] + }, { "type": "dict", "key": "schema_template_exaples", From 6d678c242b3e27075b10e0057b50c83498bc841b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:53:36 +0200 Subject: [PATCH 110/333] trigger reset on show with small delay so setting ui is visible --- openpype/tools/settings/settings/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index a60a2a1d88..f4428af6ed 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -94,7 +94,8 @@ class MainWidget(QtWidgets.QWidget): super(MainWidget, self).showEvent(event) if self._reset_on_show: self._reset_on_show = False - self.reset() + # Trigger reset with 100ms delay + QtCore.QTimer.singleShot(100, self.reset) def _show_password_dialog(self): if self._password_dialog: From 4648e145816bf6c517c4063e0aa3eccb90a25911 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:54:01 +0200 Subject: [PATCH 111/333] make sure on passed password that window is visible --- openpype/tools/settings/settings/window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index f4428af6ed..a3591f292a 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -108,6 +108,8 @@ class MainWidget(QtWidgets.QWidget): self._password_dialog = None if password_passed: self.reset() + if not self.isVisible(): + self.show() else: self.close() From 179328fbe799668e718e3b0f7b4d6a8c41f31c4c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:58:45 +0200 Subject: [PATCH 112/333] added few titles to dialogs --- openpype/tools/settings/settings/categories.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 392c749211..cf57785c25 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -289,6 +289,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): msg = "

".join(warnings) dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Save warnings") dialog.setText(msg) dialog.setIcon(QtWidgets.QMessageBox.Warning) dialog.exec_() @@ -298,6 +299,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): except Exception as exc: formatted_traceback = traceback.format_exception(*sys.exc_info()) dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Unexpected error") msg = "Unexpected error happened!\n\nError: {}".format(str(exc)) dialog.setText(msg) dialog.setDetailedText("\n".join(formatted_traceback)) @@ -387,6 +389,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): except Exception as exc: formatted_traceback = traceback.format_exception(*sys.exc_info()) dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Unexpected error") msg = "Unexpected error happened!\n\nError: {}".format(str(exc)) dialog.setText(msg) dialog.setDetailedText("\n".join(formatted_traceback)) From e8bdd1616c5aade342b02d0029d88162c9a294f6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:34:13 +0200 Subject: [PATCH 113/333] create QSettings in standalone publisher --- openpype/tools/standalonepublish/app.py | 6 +++++- openpype/tools/standalonepublish/widgets/widget_asset.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/tools/standalonepublish/app.py b/openpype/tools/standalonepublish/app.py index 169abe530a..7c3e902f6c 100644 --- a/openpype/tools/standalonepublish/app.py +++ b/openpype/tools/standalonepublish/app.py @@ -34,6 +34,8 @@ class Window(QtWidgets.QDialog): self._db = AvalonMongoDB() self._db.install() + self._settings = QtCore.QSettings("pypeclub", "StandalonePublisher") + self.pyblish_paths = pyblish_paths self.setWindowTitle("Standalone Publish") @@ -44,7 +46,9 @@ class Window(QtWidgets.QDialog): self.valid_parent = False # assets widget - widget_assets = AssetWidget(dbcon=self._db, parent=self) + widget_assets = AssetWidget( + self._settings, dbcon=self._db, parent=self + ) # family widget widget_family = FamilyWidget(dbcon=self._db, parent=self) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 4680e88344..24b93c8343 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -127,11 +127,12 @@ class AssetWidget(QtWidgets.QWidget): current_changed = QtCore.Signal() # on view current index change task_changed = QtCore.Signal() - def __init__(self, dbcon, parent=None): + def __init__(self, settings, dbcon, parent=None): super(AssetWidget, self).__init__(parent=parent) self.setContentsMargins(0, 0, 0, 0) self.dbcon = dbcon + self._settings = settings layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) From 45a6fa1ea5ab83fc6e82a6d60fea00f76990dc63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:35:28 +0200 Subject: [PATCH 114/333] store projects on project change to settings --- .../tools/standalonepublish/widgets/widget_asset.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 24b93c8343..683b05d836 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -255,6 +255,16 @@ class AssetWidget(QtWidgets.QWidget): project_name = self.combo_projects.currentText() if project_name in projects: self.dbcon.Session["AVALON_PROJECT"] = project_name + last_projects = [ + value + for value in self._settings.value("projects", "").split("|") + ] + if project_name in last_projects: + last_projects.remove(project_name) + last_projects.insert(0, project_name) + while len(last_projects) > 5: + last_projects.pop(-1) + self._settings.setValue("projects", "|".join(last_projects)) self.project_changed.emit(project_name) From 07abf855ddb40cac1ade2bc824f1784dd6f6d840 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:35:42 +0200 Subject: [PATCH 115/333] load last projects on initialization of projects combobox --- .../standalonepublish/widgets/widget_asset.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 683b05d836..0070488b3e 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -239,14 +239,31 @@ class AssetWidget(QtWidgets.QWidget): return output def _set_projects(self): - projects = list() + project_names = list() for project in self.dbcon.projects(): - projects.append(project['name']) + project_name = project.get("name") + if project_name: + project_names.append(project_name) self.combo_projects.clear() - if len(projects) > 0: - self.combo_projects.addItems(projects) - self.dbcon.Session["AVALON_PROJECT"] = projects[0] + + if not project_names: + return + + sorted_project_names = list(sorted(project_names)) + self.combo_projects.addItems(list(sorted(sorted_project_names))) + + last_projects = self._settings.value("projects", "") + last_project = sorted_project_names[0] + for project_name in last_projects.split("|"): + if project_name in sorted_project_names: + last_project = project_name + break + + index = sorted_project_names.index(last_project) + self.combo_projects.setCurrentIndex(index) + + self.dbcon.Session["AVALON_PROJECT"] = last_project def on_project_change(self): projects = list() From 72ec36239b2ad223e9e2ea64c58d17f51988715e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:52:00 +0200 Subject: [PATCH 116/333] load and store last projects is more secure --- openpype/tools/standalonepublish/app.py | 11 +++-- .../standalonepublish/widgets/widget_asset.py | 44 +++++++++++++------ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/openpype/tools/standalonepublish/app.py b/openpype/tools/standalonepublish/app.py index 7c3e902f6c..81a53c52b8 100644 --- a/openpype/tools/standalonepublish/app.py +++ b/openpype/tools/standalonepublish/app.py @@ -34,7 +34,12 @@ class Window(QtWidgets.QDialog): self._db = AvalonMongoDB() self._db.install() - self._settings = QtCore.QSettings("pypeclub", "StandalonePublisher") + try: + settings = QtCore.QSettings("pypeclub", "StandalonePublisher") + except Exception: + settings = None + + self._settings = settings self.pyblish_paths = pyblish_paths @@ -46,9 +51,7 @@ class Window(QtWidgets.QDialog): self.valid_parent = False # assets widget - widget_assets = AssetWidget( - self._settings, dbcon=self._db, parent=self - ) + widget_assets = AssetWidget(self._db, settings, self) # family widget widget_family = FamilyWidget(dbcon=self._db, parent=self) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 0070488b3e..8fb0d452bd 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -127,7 +127,7 @@ class AssetWidget(QtWidgets.QWidget): current_changed = QtCore.Signal() # on view current index change task_changed = QtCore.Signal() - def __init__(self, settings, dbcon, parent=None): + def __init__(self, dbcon, settings, parent=None): super(AssetWidget, self).__init__(parent=parent) self.setContentsMargins(0, 0, 0, 0) @@ -238,6 +238,34 @@ class AssetWidget(QtWidgets.QWidget): output.extend(self.get_parents(parent)) return output + def _get_last_projects(self): + if not self._settings: + return [] + + project_names = [] + for project_name in self._settings.value("projects", "").split("|"): + if project_name: + project_names.append(project_name) + return project_names + + def _add_last_project(self, project_name): + if not self._settings: + return + + last_projects = [] + for _project_name in self._settings.value("projects", "").split("|"): + if _project_name: + last_projects.append(_project_name) + + if project_name in last_projects: + last_projects.remove(project_name) + + last_projects.insert(0, project_name) + while len(last_projects) > 5: + last_projects.pop(-1) + + self._settings.setValue("projects", "|".join(last_projects)) + def _set_projects(self): project_names = list() for project in self.dbcon.projects(): @@ -253,9 +281,8 @@ class AssetWidget(QtWidgets.QWidget): sorted_project_names = list(sorted(project_names)) self.combo_projects.addItems(list(sorted(sorted_project_names))) - last_projects = self._settings.value("projects", "") last_project = sorted_project_names[0] - for project_name in last_projects.split("|"): + for project_name in self._get_last_projects(): if project_name in sorted_project_names: last_project = project_name break @@ -272,16 +299,7 @@ class AssetWidget(QtWidgets.QWidget): project_name = self.combo_projects.currentText() if project_name in projects: self.dbcon.Session["AVALON_PROJECT"] = project_name - last_projects = [ - value - for value in self._settings.value("projects", "").split("|") - ] - if project_name in last_projects: - last_projects.remove(project_name) - last_projects.insert(0, project_name) - while len(last_projects) > 5: - last_projects.pop(-1) - self._settings.setValue("projects", "|".join(last_projects)) + self._add_last_project(project_name) self.project_changed.emit(project_name) From f973833153676dc4dc39e43d530eb1d96e8af4f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:54:21 +0200 Subject: [PATCH 117/333] added delegate to project combobox --- openpype/tools/standalonepublish/widgets/widget_asset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 8fb0d452bd..c39d71b055 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -140,6 +140,10 @@ class AssetWidget(QtWidgets.QWidget): # Project self.combo_projects = QtWidgets.QComboBox() + # Change delegate so stylysheets are applied + project_delegate = QtWidgets.QStyledItemDelegate(self.combo_projects) + self.combo_projects.setItemDelegate(project_delegate) + self._set_projects() self.combo_projects.currentTextChanged.connect(self.on_project_change) # Tree View @@ -199,6 +203,7 @@ class AssetWidget(QtWidgets.QWidget): self.selection_changed.connect(self._refresh_tasks) + self.project_delegate = project_delegate self.task_view = task_view self.task_model = task_model self.refreshButton = refresh From 93b9624181a1257f7de917a16e04736130eef9e9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 11:21:05 +0200 Subject: [PATCH 118/333] fix key loaded from settings --- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index b505a429b5..8464a43ef7 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -51,7 +51,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): families = instance.data.get("families") add_ftrack_family = profile["add_ftrack_family"] - additional_filters = profile.get("additional_filters") + additional_filters = profile.get("advanced_filtering") if additional_filters: add_ftrack_family = self._get_add_ftrack_f_from_addit_filters( additional_filters, From 83afef7392e09010019aa5a4029046b61189cdc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 11:21:31 +0200 Subject: [PATCH 119/333] add ftrack family for review family --- openpype/settings/defaults/project_settings/ftrack.json | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 88f4e1e2e7..7f15742772 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -229,7 +229,6 @@ "standalonepublisher" ], "families": [ - "review", "plate" ], "tasks": [], From 622c6e6ca3a28e4da9251810581fd8155c9a23da Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 8 Jul 2021 12:15:09 +0200 Subject: [PATCH 120/333] nuke: fixing wrong name of family folder when `used existing frames` --- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 00d96c6cd1..662f2c808e 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -81,18 +81,18 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): if target == "Use existing frames": # Local rendering self.log.info("flagged for no render") - families.append(family) + families.append(families_ak.lower()) elif target == "Local": # Local rendering self.log.info("flagged for local render") families.append("{}.local".format(family)) + family = families_ak.lower() elif target == "On farm": # Farm rendering self.log.info("flagged for farm render") instance.data["transfer"] = False families.append("{}.farm".format(family)) - - family = families_ak.lower() + family = families_ak.lower() node.begin() for i in nuke.allNodes(): From d00eec81d4de2e47b2ee6842ec92edc3bf9c2d78 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 12:16:45 +0200 Subject: [PATCH 121/333] formatting keys fix --- openpype/hosts/houdini/plugins/publish/collect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 413553c864..2e294face2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -56,7 +56,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # Create nice name if the instance has a frame range. label = data.get("name", node.name()) if "frameStart" in data and "frameEnd" in data: - frames = "[{startFrame} - {endFrame}]".format(**data) + frames = "[{frameStart} - {frameEnd}]".format(**data) label = "{} {}".format(label, frames) instance = context.create_instance(label) From 130e9ffa502605011f2ce75d351fa2f5280f7dfe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 8 Jul 2021 12:33:09 +0200 Subject: [PATCH 122/333] nuke: fix review family switch --- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 00d96c6cd1..9d671646c5 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -70,8 +70,9 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): review = False if "review" in node.knobs(): review = node["review"].value() + + if review: families.append("review") - families.append("ftrack") # Add all nodes in group instances. if node.Class() == "Group": From 97fe86cdd1faee28f67f66e418eea8ce7c7cfd6b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 8 Jul 2021 12:37:11 +0200 Subject: [PATCH 123/333] settings: Ftrack family nuke preset --- .../defaults/project_settings/ftrack.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 88f4e1e2e7..78b34d5373 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -279,6 +279,25 @@ "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] + }, + { + "hosts": [ + "nuke" + ], + "families": [ + "write", + "render" + ], + "tasks": [], + "add_ftrack_family": false, + "advanced_filtering": [ + { + "families": [ + "review" + ], + "add_ftrack_family": true + } + ] } ] }, From 89957fca966bc95976420240117377d729fb26de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 13:59:00 +0200 Subject: [PATCH 124/333] check_inventory_versions skip representations that were not found in database --- openpype/hosts/nuke/api/lib.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index d7f3fdc6ba..9922409dd1 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -113,6 +113,14 @@ def check_inventory_versions(): "_id": io.ObjectId(avalon_knob_data["representation"]) }) + # Failsafe for not finding the representation. + if not representation: + log.warning( + "Could not find the representation on " + "node \"{}\"".format(node.name()) + ) + continue + # Get start frame from version data version = io.find_one({ "type": "version", From 54eb42f16ac024723270a3468fe72d03274b5b19 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 14:43:00 +0200 Subject: [PATCH 125/333] fix ignoring of missing defaults --- openpype/settings/entities/dict_conditional.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 641986491c..1ffc7ab450 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -384,16 +384,9 @@ class DictConditionalEntity(ItemEntity): # Set override state on enum entity first self.enum_entity.set_override_state(state, ignore_missing_defaults) - # Set override state on other entities under current enum value - for child_obj in self.non_gui_children[self.current_enum].values(): - child_obj.set_override_state(state, ignore_missing_defaults) - # Set override state on other enum children # - these must not raise exception about missing defaults for item_key, children_by_key in self.non_gui_children.items(): - if item_key == self.current_enum: - continue - for child_obj in children_by_key.values(): child_obj.set_override_state(state, True) From 696c72c34cc555a7af365c4f194a7f510b1151a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 14:44:34 +0200 Subject: [PATCH 126/333] remove unusued variable --- openpype/settings/entities/dict_conditional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 1ffc7ab450..96065b670e 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -386,7 +386,7 @@ class DictConditionalEntity(ItemEntity): # Set override state on other enum children # - these must not raise exception about missing defaults - for item_key, children_by_key in self.non_gui_children.items(): + for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): child_obj.set_override_state(state, True) From d60eeb85b4510efa7f6fc7207afa2be1e9c64e54 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 8 Jul 2021 14:58:14 +0200 Subject: [PATCH 127/333] standalone: editorial plugins rename fix gap issue https://github.com/pypeclub/OpenPype/pull/1738#issuecomment-876373865 --- .../{collect_instances.py => collect_editorial_instances.py} | 5 ++++- ..._instance_resources.py => collect_editorial_resources.py} | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) rename openpype/hosts/standalonepublisher/plugins/publish/{collect_instances.py => collect_editorial_instances.py} (98%) rename openpype/hosts/standalonepublisher/plugins/publish/{collect_instance_resources.py => collect_editorial_resources.py} (99%) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py similarity index 98% rename from openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py rename to openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index 0d95da444a..3474cbcdde 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -8,7 +8,7 @@ class CollectInstances(pyblish.api.InstancePlugin): """Collect instances from editorial's OTIO sequence""" order = pyblish.api.CollectorOrder + 0.01 - label = "Collect Instances" + label = "Collect Editorial Instances" hosts = ["standalonepublisher"] families = ["editorial"] @@ -84,6 +84,9 @@ class CollectInstances(pyblish.api.InstancePlugin): if clip.name is None: continue + if isinstance(clip, otio.schema.Gap): + continue + # skip all generators like black ampty if isinstance( clip.media_reference, diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instance_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py similarity index 99% rename from openpype/hosts/standalonepublisher/plugins/publish/collect_instance_resources.py rename to openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py index 565d066fd8..e262009637 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instance_resources.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py @@ -11,7 +11,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin): # must be after `CollectInstances` order = pyblish.api.CollectorOrder + 0.011 - label = "Collect Instance Resources" + label = "Collect Editorial Resources" hosts = ["standalonepublisher"] families = ["clip"] From f12c117f8669b381ba05dce11ec10c722c323630 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 9 Jul 2021 12:27:23 +0200 Subject: [PATCH 128/333] :window: dont add poetry to path, new installer --- tools/build.ps1 | 11 +++++------ tools/build_win_installer.ps1 | 8 -------- tools/create_env.ps1 | 11 ++++------- tools/create_zip.ps1 | 6 ++---- tools/fetch_thirdparty_libs.ps1 | 7 ++----- tools/make_docs.ps1 | 10 ++++------ tools/run_project_manager.ps1 | 4 ++-- tools/run_settings.ps1 | 4 ++-- tools/run_tests.ps1 | 6 ++---- tools/run_tray.ps1 | 4 ++-- 10 files changed, 25 insertions(+), 46 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index 89795b0a50..cc4253fe24 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -83,7 +83,8 @@ function Show-PSWarning() { function Install-Poetry() { Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Installing Poetry ... " - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - + $env:POETRY_HOME="$openpype_root\.poetry" + (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - } $art = @" @@ -115,11 +116,9 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root @@ -164,7 +163,7 @@ Write-Host " ]" -ForegroundColor white Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -184,7 +183,7 @@ Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Building OpenPype ..." $startTime = [int][double]::Parse((Get-Date -UFormat %s)) -$out = & poetry run python setup.py build 2>&1 +$out = & "$($env:POETRY_HOME)\bin\poetry" run python setup.py build 2>&1 Set-Content -Path "$($openpype_root)\build\build.log" -Value $out if ($LASTEXITCODE -ne 0) { @@ -195,7 +194,7 @@ if ($LASTEXITCODE -ne 0) } Set-Content -Path "$($openpype_root)\build\build.log" -Value $out -& poetry run python "$($openpype_root)\tools\build_dependencies.py" +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\tools\build_dependencies.py" Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "restoring current directory" diff --git a/tools/build_win_installer.ps1 b/tools/build_win_installer.ps1 index 05ec0f9823..a0832e0135 100644 --- a/tools/build_win_installer.ps1 +++ b/tools/build_win_installer.ps1 @@ -64,14 +64,6 @@ function Show-PSWarning() { } } -function Install-Poetry() { - Write-Host ">>> " -NoNewline -ForegroundColor Green - Write-Host "Installing Poetry ... " - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - - # add it to PATH - $env:PATH = "$($env:PATH);$($env:USERPROFILE)\.poetry\bin" -} - $art = @" . . .. . .. diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index 94a91ce48f..6c8124ccb2 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -49,9 +49,7 @@ function Install-Poetry() { Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Installing Poetry ... " $env:POETRY_HOME="$openpype_root\.poetry" - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - - # add it to PATH - $env:PATH = "$($env:PATH);$openpype_root\.poetry\bin" + (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - } @@ -94,11 +92,10 @@ $current_dir = Get-Location $script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent $openpype_root = (Get-Item $script_dir).parent.FullName -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" + Set-Location -Path $openpype_root @@ -145,7 +142,7 @@ Test-Python Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Install-Poetry Write-Host "INSTALLED" -ForegroundColor Cyan @@ -160,7 +157,7 @@ if (-not (Test-Path -PathType Leaf -Path "$($openpype_root)\poetry.lock")) { Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Installing virtual environment from lock." } -& poetry install --no-root $poetry_verbosity +& "$env:POETRY_HOME\bin\poetry" install --no-root $poetry_verbosity --ansi if ($LASTEXITCODE -ne 0) { Write-Host "!!! " -ForegroundColor yellow -NoNewline Write-Host "Poetry command failed." diff --git a/tools/create_zip.ps1 b/tools/create_zip.ps1 index 1a7520eb11..c27857b480 100644 --- a/tools/create_zip.ps1 +++ b/tools/create_zip.ps1 @@ -45,11 +45,9 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root @@ -87,7 +85,7 @@ if (-not $openpype_version) { Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -107,5 +105,5 @@ Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Generating zip from current sources ..." $env:PYTHONPATH="$($openpype_root);$($env:PYTHONPATH)" $env:OPENPYPE_ROOT="$($openpype_root)" -& poetry run python "$($openpype_root)\tools\create_zip.py" $ARGS +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\tools\create_zip.py" $ARGS Set-Location -Path $current_dir diff --git a/tools/fetch_thirdparty_libs.ps1 b/tools/fetch_thirdparty_libs.ps1 index 23f0b50c7a..16f7b70e7a 100644 --- a/tools/fetch_thirdparty_libs.ps1 +++ b/tools/fetch_thirdparty_libs.ps1 @@ -17,18 +17,15 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root - Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -37,5 +34,5 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { Write-Host "OK" -ForegroundColor Green } -& poetry run python "$($openpype_root)\tools\fetch_thirdparty_libs.py" +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\tools\fetch_thirdparty_libs.py" Set-Location -Path $current_dir diff --git a/tools/make_docs.ps1 b/tools/make_docs.ps1 index 2f9350eff0..45a11171ae 100644 --- a/tools/make_docs.ps1 +++ b/tools/make_docs.ps1 @@ -19,11 +19,9 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root @@ -50,7 +48,7 @@ Write-Host $art -ForegroundColor DarkGreen Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -63,10 +61,10 @@ Write-Host "This will not overwrite existing source rst files, only scan and add Set-Location -Path $openpype_root Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Running apidoc ..." -& poetry run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$($openpype_root)\docs\source" igniter -& poetry run sphinx-apidoc.exe -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$($openpype_root)\docs\source" openpype vendor, openpype\vendor +& "$env:POETRY_HOME\bin\poetry" run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$($openpype_root)\docs\source" igniter +& "$env:POETRY_HOME\bin\poetry" run sphinx-apidoc.exe -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$($openpype_root)\docs\source" openpype vendor, openpype\vendor Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Building html ..." -& poetry run python "$($openpype_root)\setup.py" build_sphinx +& "$env:POETRY_HOME\bin\poetry" run python "$($openpype_root)\setup.py" build_sphinx Set-Location -Path $current_dir diff --git a/tools/run_project_manager.ps1 b/tools/run_project_manager.ps1 index 9886a80316..a9cfbb1e7b 100644 --- a/tools/run_project_manager.ps1 +++ b/tools/run_project_manager.ps1 @@ -47,7 +47,7 @@ Set-Location -Path $openpype_root Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -56,5 +56,5 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { Write-Host "OK" -ForegroundColor Green } -& poetry run python "$($openpype_root)\start.py" projectmanager +& "$env:POETRY_HOME\bin\poetry" run python "$($openpype_root)\start.py" projectmanager Set-Location -Path $current_dir diff --git a/tools/run_settings.ps1 b/tools/run_settings.ps1 index 7477e546b3..1c0aa6e8f3 100644 --- a/tools/run_settings.ps1 +++ b/tools/run_settings.ps1 @@ -27,7 +27,7 @@ Set-Location -Path $openpype_root Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -36,5 +36,5 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { Write-Host "OK" -ForegroundColor Green } -& poetry run python "$($openpype_root)\start.py" settings --dev +& "$env:POETRY_HOME\bin\poetry" run python "$($openpype_root)\start.py" settings --dev Set-Location -Path $current_dir \ No newline at end of file diff --git a/tools/run_tests.ps1 b/tools/run_tests.ps1 index a6882e2a09..e631cb72df 100644 --- a/tools/run_tests.ps1 +++ b/tools/run_tests.ps1 @@ -59,11 +59,9 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root @@ -83,7 +81,7 @@ Write-Host " ] ..." -ForegroundColor white Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -102,7 +100,7 @@ Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Testing OpenPype ..." $original_pythonpath = $env:PYTHONPATH $env:PYTHONPATH="$($openpype_root);$($env:PYTHONPATH)" -& poetry run pytest -x --capture=sys --print -W ignore::DeprecationWarning "$($openpype_root)/tests" +& "$env:POETRY_HOME\bin\poetry" run pytest -x --capture=sys --print -W ignore::DeprecationWarning "$($openpype_root)/tests" $env:PYTHONPATH = $original_pythonpath Write-Host ">>> " -NoNewline -ForegroundColor green diff --git a/tools/run_tray.ps1 b/tools/run_tray.ps1 index 533a791836..872c1524a6 100644 --- a/tools/run_tray.ps1 +++ b/tools/run_tray.ps1 @@ -26,7 +26,7 @@ Set-Location -Path $openpype_root Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -35,5 +35,5 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { Write-Host "OK" -ForegroundColor Green } -& poetry run python "$($openpype_root)\start.py" tray --debug +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\start.py" tray --debug Set-Location -Path $current_dir \ No newline at end of file From 43f7f8276aa714a465e9970a8cd8430b38772691 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 9 Jul 2021 12:38:19 +0200 Subject: [PATCH 129/333] =?UTF-8?q?=F0=9F=90=A7=F0=9F=8D=8E=20don't=20add?= =?UTF-8?q?=20poetry=20to=20path,=20new=20installer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/build.sh | 23 +++-------------------- tools/create_env.sh | 16 +++++++--------- tools/create_zip.sh | 4 +--- tools/fetch_thirdparty_libs.sh | 7 +------ tools/make_docs.sh | 8 +++----- tools/run_projectmanager.sh | 4 +--- tools/run_settings.sh | 4 +--- tools/run_tests.sh | 4 +--- tools/run_tray.sh | 4 +--- 9 files changed, 19 insertions(+), 55 deletions(-) diff --git a/tools/build.sh b/tools/build.sh index aa8f0121ea..c44e7157af 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -140,21 +140,6 @@ realpath () { echo $(cd $(dirname "$1") || return; pwd)/$(basename "$1") } -############################################################################## -# Install Poetry when needed -# Globals: -# PATH -# Arguments: -# None -# Returns: -# None -############################################################################### -install_poetry () { - echo -e "${BIGreen}>>>${RST} Installing Poetry ..." - command -v curl >/dev/null 2>&1 || { echo -e "${BIRed}!!!${RST}${BIYellow} Missing ${RST}${BIBlue}curl${BIYellow} command.${RST}"; return 1; } - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - -} - # Main main () { echo -e "${BGreen}" @@ -171,11 +156,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIYellow}---${RST} Cleaning build directory ..." rm -rf "$openpype_root/build" && mkdir "$openpype_root/build" > /dev/null @@ -201,11 +184,11 @@ if [ "$disable_submodule_update" == 1 ]; then fi echo -e "${BIGreen}>>>${RST} Building ..." if [[ "$OSTYPE" == "linux-gnu"* ]]; then - poetry run python "$openpype_root/setup.py" build > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } + "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" build > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } elif [[ "$OSTYPE" == "darwin"* ]]; then - poetry run python "$openpype_root/setup.py" bdist_mac > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } + "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } fi - poetry run python "$openpype_root/tools/build_dependencies.py" + "$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/build_dependencies.py" if [[ "$OSTYPE" == "darwin"* ]]; then # fix code signing issue diff --git a/tools/create_env.sh b/tools/create_env.sh index 226a26e199..cc9eddc317 100755 --- a/tools/create_env.sh +++ b/tools/create_env.sh @@ -109,8 +109,7 @@ install_poetry () { echo -e "${BIGreen}>>>${RST} Installing Poetry ..." export POETRY_HOME="$openpype_root/.poetry" command -v curl >/dev/null 2>&1 || { echo -e "${BIRed}!!!${RST}${BIYellow} Missing ${RST}${BIBlue}curl${BIYellow} command.${RST}"; return 1; } - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - - export PATH="$PATH:$POETRY_HOME/bin" + curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - } ############################################################################## @@ -154,11 +153,10 @@ main () { # Directories openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) - # make sure Poetry is in PATH + if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" pushd "$openpype_root" > /dev/null || return > /dev/null @@ -177,7 +175,7 @@ main () { echo -e "${BIGreen}>>>${RST} Installing dependencies ..." fi - poetry install --no-root $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return; } + "$POETRY_HOME/bin/poetry" install --no-root $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return; } echo -e "${BIGreen}>>>${RST} Cleaning cache files ..." clean_pyc @@ -186,10 +184,10 @@ main () { # cx_freeze will crash on missing __pychache__ on these but # reinstalling them solves the problem. echo -e "${BIGreen}>>>${RST} Fixing pycache bug ..." - poetry run python -m pip install --force-reinstall pip - poetry run pip install --force-reinstall setuptools - poetry run pip install --force-reinstall wheel - poetry run python -m pip install --force-reinstall pip + "$POETRY_HOME/bin/poetry" run python -m pip install --force-reinstall pip + "$POETRY_HOME/bin/poetry" run pip install --force-reinstall setuptools + "$POETRY_HOME/bin/poetry" run pip install --force-reinstall wheel + "$POETRY_HOME/bin/poetry" run python -m pip install --force-reinstall pip } main -3 diff --git a/tools/create_zip.sh b/tools/create_zip.sh index ec0276b040..85ee18a839 100755 --- a/tools/create_zip.sh +++ b/tools/create_zip.sh @@ -114,11 +114,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" pushd "$openpype_root" > /dev/null || return > /dev/null @@ -134,7 +132,7 @@ main () { echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..." PYTHONPATH="$openpype_root:$PYTHONPATH" OPENPYPE_ROOT="$openpype_root" - poetry run python3 "$openpype_root/tools/create_zip.py" "$@" + "$POETRY_HOME/bin/poetry" run python3 "$openpype_root/tools/create_zip.py" "$@" } main "$@" diff --git a/tools/fetch_thirdparty_libs.sh b/tools/fetch_thirdparty_libs.sh index 31f109ba68..93d0674965 100755 --- a/tools/fetch_thirdparty_libs.sh +++ b/tools/fetch_thirdparty_libs.sh @@ -1,8 +1,5 @@ #!/usr/bin/env bash -# Run Pype Tray - - art () { cat <<-EOF @@ -82,11 +79,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" if [ -f "$POETRY_HOME/bin/poetry" ]; then @@ -100,7 +95,7 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null echo -e "${BIGreen}>>>${RST} Running Pype tool ..." - poetry run python "$openpype_root/tools/fetch_thirdparty_libs.py" + "$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/fetch_thirdparty_libs.py" } main \ No newline at end of file diff --git a/tools/make_docs.sh b/tools/make_docs.sh index 9dfab26a38..52ee57dcf0 100755 --- a/tools/make_docs.sh +++ b/tools/make_docs.sh @@ -83,11 +83,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" if [ -f "$POETRY_HOME/bin/poetry" ]; then @@ -101,11 +99,11 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null echo -e "${BIGreen}>>>${RST} Running apidoc ..." - poetry run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$openpype_root/docs/source" igniter - poetry run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$openpype_root/docs/source" openpype vendor, openpype\vendor + "$POETRY_HOME/bin/poetry" run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$openpype_root/docs/source" igniter + "$POETRY_HOME/bin/poetry" run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$openpype_root/docs/source" openpype vendor, openpype\vendor echo -e "${BIGreen}>>>${RST} Building html ..." - poetry run python3 "$openpype_root/setup.py" build_sphinx + "$POETRY_HOME/bin/poetry" run python3 "$openpype_root/setup.py" build_sphinx } main diff --git a/tools/run_projectmanager.sh b/tools/run_projectmanager.sh index 312f321d67..b5c858c34a 100755 --- a/tools/run_projectmanager.sh +++ b/tools/run_projectmanager.sh @@ -79,11 +79,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" pushd "$openpype_root" > /dev/null || return > /dev/null @@ -97,7 +95,7 @@ main () { fi echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..." - poetry run python "$openpype_root/start.py" projectmanager + "$POETRY_HOME/bin/poetry" run python "$openpype_root/start.py" projectmanager } main diff --git a/tools/run_settings.sh b/tools/run_settings.sh index 0287043bb6..5a465dce2c 100755 --- a/tools/run_settings.sh +++ b/tools/run_settings.sh @@ -79,11 +79,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" pushd "$openpype_root" > /dev/null || return > /dev/null @@ -97,7 +95,7 @@ main () { fi echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..." - poetry run python3 "$openpype_root/start.py" settings --dev + "$POETRY_HOME/bin/poetry" run python3 "$openpype_root/start.py" settings --dev } main diff --git a/tools/run_tests.sh b/tools/run_tests.sh index 90977edc83..8f8f82fd9c 100755 --- a/tools/run_tests.sh +++ b/tools/run_tests.sh @@ -98,11 +98,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" if [ -f "$POETRY_HOME/bin/poetry" ]; then @@ -118,7 +116,7 @@ main () { echo -e "${BIGreen}>>>${RST} Testing OpenPype ..." original_pythonpath=$PYTHONPATH export PYTHONPATH="$openpype_root:$PYTHONPATH" - poetry run pytest -x --capture=sys --print -W ignore::DeprecationWarning "$openpype_root/tests" + "$POETRY_HOME/bin/poetry" run pytest -x --capture=sys --print -W ignore::DeprecationWarning "$openpype_root/tests" PYTHONPATH=$original_pythonpath } diff --git a/tools/run_tray.sh b/tools/run_tray.sh index 339ff6f918..2eb9886063 100755 --- a/tools/run_tray.sh +++ b/tools/run_tray.sh @@ -56,11 +56,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" if [ -f "$POETRY_HOME/bin/poetry" ]; then @@ -74,7 +72,7 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null echo -e "${BIGreen}>>>${RST} Running OpenPype Tray with debug option ..." - poetry run python3 "$openpype_root/start.py" tray --debug + "$POETRY_HOME/bin/poetry" run python3 "$openpype_root/start.py" tray --debug } main \ No newline at end of file From a7d5c63228c2e3b56a16fa416653953f4e367237 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 9 Jul 2021 12:50:16 +0200 Subject: [PATCH 130/333] =?UTF-8?q?=F0=9F=90=9E:=20fix=20yeti=20settings?= =?UTF-8?q?=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/plugins/publish/extract_yeti_rig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index b9bed47fa5..eef3c4e9af 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -192,12 +192,12 @@ class ExtractYetiRig(openpype.api.Extractor): 'stagingDir': dirname } ) - self.log.info("settings file: {}".format(settings)) + self.log.info("settings file: {}".format(settings_path)) instance.data["representations"].append( { 'name': 'rigsettings', 'ext': 'rigsettings', - 'files': os.path.basename(settings), + 'files': os.path.basename(settings_path), 'stagingDir': dirname } ) From b7882ba8337f1afc09a569636a42bb5e6257210b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 9 Jul 2021 15:29:06 +0200 Subject: [PATCH 131/333] Fix - added better validation and documentation --- .../publish/validate_instance_asset.py | 61 +++++++++++++++++++ .../publish/validate_instance_asset.py | 24 +++++--- 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py new file mode 100644 index 0000000000..eff89adcb3 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py @@ -0,0 +1,61 @@ +from avalon import api +import pyblish.api +import openpype.api +from avalon import aftereffects + + +class ValidateInstanceAssetRepair(pyblish.api.Action): + """Repair the instance asset with value from Context.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + stub = aftereffects.stub() + for instance in instances: + data = stub.read(instance[0]) + + data["asset"] = api.Session["AVALON_ASSET"] + stub.imprint(instance[0], data) + + +class ValidateInstanceAsset(pyblish.api.InstancePlugin): + """Validate the instance asset is the current selected context asset. + + As it might happen that multiple worfiles are opened at same time, + switching between them would mess with selected context. (From Launcher + or Ftrack). + + In that case outputs might be output under wrong asset! + + Repair action will use Context asset value (from Workfiles or Launcher) + Closing and reopening with Workfiles will refresh Context value. + """ + + label = "Validate Instance Asset" + hosts = ["aftereffects"] + actions = [ValidateInstanceAssetRepair] + order = openpype.api.ValidateContentsOrder + + def process(self, instance): + instance_asset = instance.data["asset"] + current_asset = api.Session["AVALON_ASSET"] + msg = ( + f"Instance asset {instance_asset} is not the same " + f"as current context {current_asset}. PLEASE DO:\n" + f"Repair with 'A' action to use '{current_asset}'.\n" + f"If that's not correct value, close workfile and " + f"reopen via Workfiles!" + ) + assert instance_asset == current_asset, msg diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py index a1de02f319..4dc1972074 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -1,5 +1,4 @@ -import os - +from avalon import api import pyblish.api import openpype.api from avalon import photoshop @@ -27,12 +26,20 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): for instance in instances: data = stub.read(instance[0]) - data["asset"] = os.environ["AVALON_ASSET"] + data["asset"] = api.Session["AVALON_ASSET"] stub.imprint(instance[0], data) class ValidateInstanceAsset(pyblish.api.InstancePlugin): - """Validate the instance asset is the current asset.""" + """Validate the instance asset is the current selected context asset. + + As it might happen that multiple worfiles are opened, switching + between them would mess with selected context. + In that case outputs might be output under wrong asset! + + Repair action will use Context asset value (from Workfiles or Launcher) + Closing and reopening with Workfiles will refresh Context value. + """ label = "Validate Instance Asset" hosts = ["photoshop"] @@ -41,9 +48,12 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin): def process(self, instance): instance_asset = instance.data["asset"] - current_asset = os.environ["AVALON_ASSET"] + current_asset = api.Session["AVALON_ASSET"] msg = ( - "Instance asset is not the same as current asset:" - f"\nInstance: {instance_asset}\nCurrent: {current_asset}" + f"Instance asset {instance_asset} is not the same " + f"as current context {current_asset}. PLEASE DO:\n" + f"Repair with 'A' action to use '{current_asset}'.\n" + f"If that's not correct value, close workfile and " + f"reopen via Workfiles!" ) assert instance_asset == current_asset, msg From 39efe7e105b9d6d41cb9e16902086a7f97d09838 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 10 Jul 2021 03:41:06 +0000 Subject: [PATCH 132/333] [Automated] Bump version --- CHANGELOG.md | 57 +++++++++++++++------------------------------ openpype/version.py | 2 +- 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e658d6995..5e76d7b76a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,16 @@ # Changelog -## [3.2.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) +- Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799) +- Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) +- Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) - Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776) -- Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769) - Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766) - Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) @@ -16,18 +19,18 @@ - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) -- Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) -- Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) - Toggle Ftrack upload in StandalonePublisher [\#1708](https://github.com/pypeclub/OpenPype/pull/1708) -- Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) -- Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699) -- Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695) **🐛 Bug fixes** +- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) +- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) +- Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) +- Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) - Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) - FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775) - Fix - single file files are str only, cast it to list to count properly [\#1772](https://github.com/pypeclub/OpenPype/pull/1772) +- Environments in app executable for MacOS [\#1768](https://github.com/pypeclub/OpenPype/pull/1768) - Project specific environments [\#1767](https://github.com/pypeclub/OpenPype/pull/1767) - Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764) - Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761) @@ -36,22 +39,18 @@ - hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743) - Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) - Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741) +- StandalonePublisher: failing collector for editorial [\#1738](https://github.com/pypeclub/OpenPype/pull/1738) - Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) - TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735) -- Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734) -- Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733) -- Ftrack sync status [\#1732](https://github.com/pypeclub/OpenPype/pull/1732) -- TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) -- Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) -- Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) -- Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) - Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) +- Unreal: launching on Linux [\#1672](https://github.com/pypeclub/OpenPype/pull/1672) **Merged pull requests:** +- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808) - Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) +- Bc/fix/docs [\#1771](https://github.com/pypeclub/OpenPype/pull/1771) - TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) -- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) @@ -66,26 +65,21 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) -**🚀 Enhancements** - -- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - **🐛 Bug fixes** - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) +**Merged pull requests:** + +- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) + ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2) -**🚀 Enhancements** - -- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650) - **🐛 Bug fixes** - Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713) -- StandalonePublisher: instance data attribute `keepSequence` [\#1668](https://github.com/pypeclub/OpenPype/pull/1668) **Merged pull requests:** @@ -97,19 +91,12 @@ **🚀 Enhancements** +- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) - OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) - Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) - \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683) - Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682) -- Project Manger: Default name column width [\#1669](https://github.com/pypeclub/OpenPype/pull/1669) -- Remove outline in stylesheet [\#1667](https://github.com/pypeclub/OpenPype/pull/1667) -- TVPaint: Creator take layer name as default value for subset variant [\#1663](https://github.com/pypeclub/OpenPype/pull/1663) -- TVPaint custom subset template [\#1662](https://github.com/pypeclub/OpenPype/pull/1662) -- Editorial: conform assets validator [\#1659](https://github.com/pypeclub/OpenPype/pull/1659) -- Feature Slack integration [\#1657](https://github.com/pypeclub/OpenPype/pull/1657) -- Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653) -- \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649) **🐛 Bug fixes** @@ -119,15 +106,9 @@ - Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) -- Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) -- Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) -- Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) -- Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) -- Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) **Merged pull requests:** -- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) diff --git a/openpype/version.py b/openpype/version.py index 86d62a83d0..dabeacc084 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.6" +__version__ = "3.2.0-nightly.7" From 500a2548035a00e82b814ee297f46a6885b89f72 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:36:20 +0200 Subject: [PATCH 133/333] rawjson entity can store value as string --- openpype/settings/entities/input_entities.py | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 2abb7a2253..6952529963 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -1,5 +1,6 @@ import re import copy +import json from abc import abstractmethod from .base_entity import ItemEntity @@ -440,6 +441,7 @@ class RawJsonEntity(InputEntity): def _item_initalization(self): # Schema must define if valid value is dict or list + store_as_string = self.schema_data.get("store_as_string", False) is_list = self.schema_data.get("is_list", False) if is_list: valid_value_types = (list, ) @@ -448,6 +450,8 @@ class RawJsonEntity(InputEntity): valid_value_types = (dict, ) value_on_not_set = {} + self.store_as_string = store_as_string + self._is_list = is_list self.valid_value_types = valid_value_types self.value_on_not_set = value_on_not_set @@ -491,6 +495,23 @@ class RawJsonEntity(InputEntity): result = self.metadata != self._metadata_for_current_state() return result + def schema_validations(self): + if self.store_as_string and self.is_env_group: + reason = ( + "RawJson entity can't store environment group metadata" + " as string." + ) + raise EntitySchemaError(self, reason) + super(RawJsonEntity, self).schema_validations() + + def _convert_to_valid_type(self, value): + if isinstance(value, STRING_TYPE): + try: + return json.loads(value) + except Exception: + pass + return super(RawJsonEntity, self)._convert_to_valid_type(value) + def _metadata_for_current_state(self): if ( self._override_state is OverrideState.PROJECT @@ -510,6 +531,9 @@ class RawJsonEntity(InputEntity): value = super(RawJsonEntity, self)._settings_value() if self.is_env_group and isinstance(value, dict): value.update(self.metadata) + + if self.store_as_string: + return json.dumps(value) return value def _prepare_value(self, value): From 27bfa50da6d6c8fa1c566e89f3ba250d26aff6d0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:36:35 +0200 Subject: [PATCH 134/333] store project folder structure as text --- .../schemas/projects_schema/schema_project_global.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json index 6e5cf0671c..a8bce47592 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json @@ -17,7 +17,8 @@ "type": "raw-json", "label": "Project Folder Structure", "key": "project_folder_structure", - "use_label_wrap": true + "use_label_wrap": true, + "store_as_string": true }, { "type": "schema", From c55d67bb58e00a01eeaf885e935c03f1869163fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:36:51 +0200 Subject: [PATCH 135/333] action where project_folder_structure is used expect string value --- .../event_handlers_user/action_create_project_structure.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py index d7ac866e42..035a1c60de 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py @@ -1,5 +1,6 @@ import os import re +import json from openpype.modules.ftrack.lib import BaseAction, statics_icon from openpype.api import Anatomy, get_project_settings @@ -84,6 +85,9 @@ class CreateProjectFolders(BaseAction): } try: + if isinstance(project_folder_structure, str): + project_folder_structure = json.loads(project_folder_structure) + # Get paths based on presets basic_paths = self.get_path_items(project_folder_structure) self.create_folders(basic_paths, project_entity) From 62782b4db6361ec18632400c4b322412767f5b14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:40:09 +0200 Subject: [PATCH 136/333] resaved defaults --- .../defaults/project_settings/global.json | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 037fa63a29..6771dfabf8 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -271,28 +271,7 @@ } } }, - "project_folder_structure": { - "__project_root__": { - "prod": {}, - "resources": { - "footage": { - "plates": {}, - "offline": {} - }, - "audio": {}, - "art_dept": {} - }, - "editorial": {}, - "assets[ftrack.Library]": { - "characters[ftrack]": {}, - "locations[ftrack]": {} - }, - "shots[ftrack.Sequence]": { - "scripts": {}, - "editorial[ftrack.Folder]": {} - } - } - }, + "project_folder_structure": "{\"__project_root__\": {\"prod\": {}, \"resources\": {\"footage\": {\"plates\": {}, \"offline\": {}}, \"audio\": {}, \"art_dept\": {}}, \"editorial\": {}, \"assets[ftrack.Library]\": {\"characters[ftrack]\": {}, \"locations[ftrack]\": {}}, \"shots[ftrack.Sequence]\": {\"scripts\": {}, \"editorial[ftrack.Folder]\": {}}}}", "sync_server": { "enabled": true, "config": { From 470b3d4add0cce3592c9ec3cd688cf818698c489 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:43:17 +0200 Subject: [PATCH 137/333] added store_as_string to readme --- openpype/settings/entities/schemas/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 3c360b892f..d457e44e74 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -337,6 +337,11 @@ How output of the schema could look like on save: - schema also defines valid value type - by default it is dictionary - to be able use list it is required to define `is_list` to `true` +- output can be stored as string + - this is to allow any keys in dictionary + - set key `store_as_string` to `true` + - code using that setting must expected that value is string and use json module to convert it to python types + ``` { "type": "raw-json", From 6e0a51fa7c39a491668bd4e04195ef5a7d7f8154 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jul 2021 12:28:05 +0200 Subject: [PATCH 138/333] remove unnecessary if --- openpype/hosts/maya/plugins/publish/extract_yeti_rig.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index eef3c4e9af..56d5dfe901 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -133,10 +133,10 @@ class ExtractYetiRig(openpype.api.Extractor): image_search_path = resources_dir = instance.data["resourcesDir"] settings = instance.data.get("rigsettings", None) - if settings: - settings["imageSearchPath"] = image_search_path - with open(settings_path, "w") as fp: - json.dump(settings, fp, ensure_ascii=False) + assert settings, "Yeti rig settings were not collected." + settings["imageSearchPath"] = image_search_path + with open(settings_path, "w") as fp: + json.dump(settings, fp, ensure_ascii=False) # add textures to transfers if 'transfers' not in instance.data: From 1742904d920d420d56ebc47e6cd2ccd35c04805e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 12 Jul 2021 12:46:39 +0200 Subject: [PATCH 139/333] Textures publishing - copy from 2.x --- .../plugins/publish/collect_texture.py | 240 ++++++++++++------ .../plugins/publish/validate_texture_batch.py | 47 +--- .../plugins/publish/validate_texture_name.py | 50 ++++ .../publish/validate_texture_versions.py | 24 ++ .../publish/validate_texture_workfiles.py | 22 ++ 5 files changed, 273 insertions(+), 110 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 0e2b21927f..b8f8f05dc9 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -9,6 +9,11 @@ from avalon.api import format_template_with_optional_keys class CollectTextures(pyblish.api.ContextPlugin): """Collect workfile (and its resource_files) and textures. + Currently implements use case with Mari and Substance Painter, where + one workfile is main (.mra - Mari) with possible additional workfiles + (.spp - Substance) + + Provides: 1 instance per workfile (with 'resources' filled if needed) (workfile family) @@ -22,40 +27,39 @@ class CollectTextures(pyblish.api.ContextPlugin): families = ["texture_batch"] actions = [] + # from presets main_workfile_extensions = ['mra'] other_workfile_extensions = ['spp', 'psd'] texture_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga", "gif", "svg"] - color_space = ["lin_srgb", "raw", "acesg"] + # additional families (ftrack etc.) + workfile_families = [] + textures_families = [] - version_regex = re.compile(r"v([0-9]+)") - udim_regex = re.compile(r"_1[0-9]{3}\.") + color_space = ["linsRGB", "raw", "acesg"] #currently implemented placeholders ["color_space"] + # describing patterns in file names splitted by regex groups input_naming_patterns = { - # workfile: ctr_envCorridorMain_texturing_v005.mra > - # expected groups: [(asset),(filler),(version)] - # texture: T_corridorMain_aluminium1_BaseColor_lin_srgb_1029.exr - # expected groups: [(asset), (filler),(color_space),(udim)] - r'^ctr_env([^.]+)_(.+)_v([0-9]{3,}).+': - r'^T_([^_.]+)_(.*)_({color_space})_(1[0-9]{3}).+' + # workfile: corridorMain_v001.mra > + # texture: corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr + r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+': + r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', + } + # matching regex group position to 'input_naming_patterns' + input_naming_groups = { + ('asset', 'filler', 'version'): + ('asset', 'shader', 'version', 'channel', 'color_space', 'udim') } workfile_subset_template = "textures{}Workfile" - # implemented keys: ["color_space", "channel", "subset"] - texture_subset_template = "textures{subset}_{channel}" - - version_regex = re.compile(r"^(.+)_v([0-9]+)") - udim_regex = re.compile(r"_1[0-9]{3}\.") + # implemented keys: ["color_space", "channel", "subset", "shader"] + texture_subset_template = "textures{subset}_{shader}_{channel}" def process(self, context): self.context = context - import json - def convertor(value): - return str(value) - resource_files = {} workfile_files = {} representations = {} @@ -76,8 +80,6 @@ class CollectTextures(pyblish.api.ContextPlugin): workfile_subset = self.workfile_subset_template.format( parsed_subset) - self.log.info("instance.data:: {}".format( - json.dumps(instance.data, indent=4, default=convertor))) processed_instance = False for repre in instance.data["representations"]: ext = repre["ext"].replace('.', '') @@ -94,9 +96,15 @@ class CollectTextures(pyblish.api.ContextPlugin): asset_build = self._get_asset_build( repre_file, self.input_naming_patterns.keys(), + self.input_naming_groups.keys(), + self.color_space + ) + version = self._get_version( + repre_file, + self.input_naming_patterns.keys(), + self.input_naming_groups.keys(), self.color_space ) - version = self._get_version(repre_file, self.version_regex) asset_builds.add((asset_build, version, workfile_subset, 'workfile')) processed_instance = True @@ -105,14 +113,17 @@ class CollectTextures(pyblish.api.ContextPlugin): representations[workfile_subset] = [] if ext in self.main_workfile_extensions: - representations[workfile_subset].append(repre) + # workfiles can have only single representation + # currently OP is not supporting different extensions in + # representation files + representations[workfile_subset] = [repre] + workfile_files[asset_build] = repre_file if ext in self.other_workfile_extensions: - self.log.info("other") # add only if not added already from main if not representations.get(workfile_subset): - representations[workfile_subset].append(repre) + representations[workfile_subset] = [repre] # only overwrite if not present if not workfile_files.get(asset_build): @@ -135,39 +146,49 @@ class CollectTextures(pyblish.api.ContextPlugin): channel = self._get_channel_name( repre_file, - list(self.input_naming_patterns.values()), + self.input_naming_patterns.values(), + self.input_naming_groups.values(), + self.color_space + ) + + shader = self._get_shader_name( + repre_file, + self.input_naming_patterns.values(), + self.input_naming_groups.values(), self.color_space ) formatting_data = { "color_space": c_space, "channel": channel, + "shader": shader, "subset": parsed_subset } - self.log.debug("data::{}".format(formatting_data)) subset = format_template_with_optional_keys( formatting_data, self.texture_subset_template) asset_build = self._get_asset_build( repre_file, self.input_naming_patterns.values(), + self.input_naming_groups.values(), + self.color_space + ) + version = self._get_version( + repre_file, + self.input_naming_patterns.values(), + self.input_naming_groups.values(), self.color_space ) - version = self._get_version(repre_file, self.version_regex) - if not representations.get(subset): representations[subset] = [] representations[subset].append(repre) - udim = self._parse_udim(repre_file, self.udim_regex) - - if not version_data.get(subset): - version_data[subset] = [] ver_data = { "color_space": c_space, - "UDIM": udim, + "channel_name": channel, + "shader_name": shader } - version_data[subset].append(ver_data) + version_data[subset] = ver_data asset_builds.add( (asset_build, version, subset, "textures")) @@ -176,7 +197,6 @@ class CollectTextures(pyblish.api.ContextPlugin): if processed_instance: self.context.remove(instance) - self.log.info("asset_builds:: {}".format(asset_builds)) self._create_new_instances(context, asset, asset_builds, @@ -195,9 +215,13 @@ class CollectTextures(pyblish.api.ContextPlugin): asset (string): selected asset from SP asset_builds (set) of tuples (asset_build, version, subset, family) - resource_files (list) of resource dicts - representations (dict) of representation files, key is - asset_build + resource_files (list) of resource dicts - to store additional + files to main workfile + representations (list) of dicts - to store workfile info OR + all collected texture files, key is asset_build + version_data (dict) - prepared to store into version doc in DB + workfile_files (dict) - to store workfile to add to textures + key is asset_build """ # sort workfile first asset_builds = sorted(asset_builds, @@ -217,28 +241,38 @@ class CollectTextures(pyblish.api.ContextPlugin): "name": subset, "family": family, "version": int(version or main_version), - "families": [] + "asset_build": asset_build # remove in validator } ) - if resource_files.get(subset): - new_instance.data.update({ - "resources": resource_files.get(subset) - }) - workfile = workfile_files.get(asset_build) + workfile = workfile_files.get(asset_build, "DUMMY") + + if resource_files.get(subset): + # add resources only when workfile is main style + for ext in self.main_workfile_extensions: + if ext in workfile: + new_instance.data.update({ + "resources": resource_files.get(subset) + }) + break # store origin if family == 'workfile': + families = self.workfile_families + new_instance.data["source"] = "standalone publisher" else: + families = self.textures_families + repre = representations.get(subset)[0] new_instance.context.data["currentFile"] = os.path.join( repre["stagingDir"], workfile) + new_instance.data["families"] = families + # add data for version document ver_data = version_data.get(subset) if ver_data: - ver_data = ver_data[0] if workfile: ver_data['workfile'] = workfile @@ -253,7 +287,13 @@ class CollectTextures(pyblish.api.ContextPlugin): new_instance.data["representations"] = upd_representations - def _get_asset_build(self, name, input_naming_patterns, color_spaces): + self.log.debug("new instance - {}:: {}".format( + family, + json.dumps(new_instance.data, indent=4))) + + def _get_asset_build(self, name, + input_naming_patterns, input_naming_groups, + color_spaces): """Loops through configured workfile patterns to find asset name. Asset name used to bind workfile and its textures. @@ -262,35 +302,34 @@ class CollectTextures(pyblish.api.ContextPlugin): name (str): workfile name input_naming_patterns (list): [workfile_pattern] or [texture_pattern] + input_naming_groups (list) + ordinal position of regex groups matching to input_naming.. + color_spaces (list) - predefined color spaces """ - for input_pattern in input_naming_patterns: - for cs in color_spaces: - pattern = input_pattern.replace('{color_space}', cs) - regex_result = re.findall(pattern, name) + asset_name = "NOT_AVAIL" - if regex_result: - asset_name = regex_result[0][0].lower() - return asset_name + return self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'asset') or asset_name - raise ValueError("Couldnt find asset name in {}".format(name)) + def _get_version(self, name, input_naming_patterns, input_naming_groups, + color_spaces): + found = self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'version') - def _get_version(self, name, version_regex): - found = re.search(version_regex, name) if found: - return found.group().replace("v", "") + return found.replace('v', '') self.log.info("No version found in the name {}".format(name)) - def _get_udim(self, name, udim_regex): - """Parses from 'name' udim value with 'udim_regex'.""" - regex_result = udim_regex.findall(name) - udim = None - if not regex_result: - self.log.warning("Didn't find UDIM in {}".format(name)) - else: - udim = re.sub("[^0-9]", '', regex_result[0]) + def _get_udim(self, name, input_naming_patterns, input_naming_groups, + color_spaces): + """Parses from 'name' udim value.""" + found = self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'udim') + if found: + return found - return udim + self.log.warning("Didn't find UDIM in {}".format(name)) def _get_color_space(self, name, color_spaces): """Looks for color_space from a list in a file name. @@ -314,18 +353,65 @@ class CollectTextures(pyblish.api.ContextPlugin): return color_space - def _get_channel_name(self, name, input_naming_patterns, color_spaces): + def _get_shader_name(self, name, input_naming_patterns, + input_naming_groups, color_spaces): + """Return parsed shader name. + + Shader name is needed for overlapping udims (eg. udims might be + used for different materials, shader needed to not overwrite). + + Unknown format of channel name and color spaces >> cs are known + list - 'color_space' used as a placeholder + """ + found = self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'shader') + if found: + return found + + self.log.warning("Didn't find shader in {}".format(name)) + + def _get_channel_name(self, name, input_naming_patterns, + input_naming_groups, color_spaces): """Return parsed channel name. Unknown format of channel name and color spaces >> cs are known list - 'color_space' used as a placeholder """ - for texture_pattern in input_naming_patterns: + found = self._parse(name, input_naming_patterns, input_naming_groups, + color_spaces, 'channel') + if found: + return found + + self.log.warning("Didn't find channel in {}".format(name)) + + def _parse(self, name, input_naming_patterns, input_naming_groups, + color_spaces, key): + """Universal way to parse 'name' with configurable regex groups. + + Args: + name (str): workfile name + input_naming_patterns (list): + [workfile_pattern] or [texture_pattern] + input_naming_groups (list) + ordinal position of regex groups matching to input_naming.. + color_spaces (list) - predefined color spaces + + Raises: + ValueError - if broken 'input_naming_groups' + """ + for input_pattern in input_naming_patterns: for cs in color_spaces: - pattern = texture_pattern.replace('{color_space}', cs) - ret = re.findall(pattern, name) - if ret: - return ret.pop()[1] + pattern = input_pattern.replace('{color_space}', cs) + regex_result = re.findall(pattern, name) + if regex_result: + idx = list(input_naming_groups)[0].index(key) + if idx < 0: + msg = "input_naming_groups must " +\ + "have '{}' key".format(key) + raise ValueError(msg) + + parsed_value = regex_result[0][idx] + return parsed_value def _update_representations(self, upd_representations): """Frames dont have sense for textures, add collected udims instead.""" @@ -335,15 +421,21 @@ class CollectTextures(pyblish.api.ContextPlugin): repre.pop("frameEnd", None) repre.pop("fps", None) + # ignore unique name from SP, use extension instead + # SP enforces unique name, here different subsets >> unique repres + repre["name"] = repre["ext"].replace('.', '') + files = repre.get("files", []) if not isinstance(files, list): files = [files] for file_name in files: - udim = self._get_udim(file_name, self.udim_regex) + udim = self._get_udim(file_name, + self.input_naming_patterns.values(), + self.input_naming_groups.values(), + self.color_space) udims.append(udim) repre["udim"] = udims # must be this way, used for filling path return upd_representations - diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py index e222004456..af200b59e0 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py @@ -2,46 +2,21 @@ import pyblish.api import openpype.api -class ValidateTextureBatch(pyblish.api.ContextPlugin): - """Validates that collected instnaces for Texture batch are OK. +class ValidateTextureBatch(pyblish.api.InstancePlugin): + """Validates that some texture files are present.""" - Validates: - some textures are present - workfile has resource files (optional) - texture version matches to workfile version - """ - - label = "Validate Texture Batch" + label = "Validate Texture Presence" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder - families = ["workfile", "textures"] - - def process(self, context): - - workfiles = [] - workfiles_in_textures = [] - for instance in context: - if instance.data["family"] == "workfile": - workfiles.append(instance.data["representations"][0]["files"]) - - if not instance.data.get("resources"): - msg = "No resources for workfile {}".\ - format(instance.data["name"]) - self.log.warning(msg) + families = ["workfile"] + optional = False + def process(self, instance): + present = False + for instance in instance.context: if instance.data["family"] == "textures": - wfile = instance.data["versionData"]["workfile"] - workfiles_in_textures.append(wfile) + self.log.info("Some textures present.") - version_str = "v{:03d}".format(instance.data["version"]) - assert version_str in wfile, \ - "Not matching version, texture {} - workfile {}".format( - instance.data["version"], wfile - ) + return - msg = "Not matching set of workfiles and textures." + \ - "{} not equal to {}".format(set(workfiles), - set(workfiles_in_textures)) +\ - "\nCheck that both workfile and textures are present" - keys = set(workfiles) == set(workfiles_in_textures) - assert keys, msg + assert present, "No textures found in published batch!" diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py new file mode 100644 index 0000000000..92f930c3fc --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py @@ -0,0 +1,50 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureBatchNaming(pyblish.api.InstancePlugin): + """Validates that all instances had properly formatted name.""" + + label = "Validate Texture Batch Naming" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["workfile", "textures"] + optional = False + + def process(self, instance): + file_name = instance.data["representations"][0]["files"] + if isinstance(file_name, list): + file_name = file_name[0] + + msg = "Couldnt find asset name in '{}'\n".format(file_name) + \ + "File name doesn't follow configured pattern.\n" + \ + "Please rename the file." + assert "NOT_AVAIL" not in instance.data["asset_build"], msg + + instance.data.pop("asset_build") + + if instance.data["family"] == "textures": + file_name = instance.data["representations"][0]["files"][0] + self._check_proper_collected(instance.data["versionData"], + file_name) + + def _check_proper_collected(self, versionData, file_name): + """ + Loop through collected versionData to check if name parsing was OK. + Args: + versionData: (dict) + + Returns: + raises AssertionException + """ + missing_key_values = [] + for key, value in versionData.items(): + if not value: + missing_key_values.append(key) + + msg = "Collected data {} doesn't contain values for {}".format( + versionData, missing_key_values) + "\n" + \ + "Name of the texture file doesn't match expected pattern.\n" + \ + "Please rename file(s) {}".format(file_name) + + assert not missing_key_values, msg diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py new file mode 100644 index 0000000000..3985cb8933 --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py @@ -0,0 +1,24 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): + """Validates that versions match in workfile and textures.""" + label = "Validate Texture Batch Versions" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["textures"] + optional = True + + def process(self, instance): + wfile = instance.data["versionData"]["workfile"] + + version_str = "v{:03d}".format(instance.data["version"]) + if 'DUMMY' in wfile: + self.log.warning("Textures are missing attached workfile") + else: + msg = "Not matching version: texture v{:03d} - workfile {}" + assert version_str in wfile, \ + msg.format( + instance.data["version"], wfile + ) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py new file mode 100644 index 0000000000..556a73dc4f --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -0,0 +1,22 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): + """Validates that textures workfile has collected resources (optional). + + Collected recourses means secondary workfiles (in most cases). + """ + + label = "Validate Texture Workfile" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["workfile"] + optional = True + + def process(self, instance): + if instance.data["family"] == "workfile": + if not instance.data.get("resources"): + msg = "No resources for workfile {}".\ + format(instance.data["name"]) + self.log.warning(msg) From 4b2aba2f450dfc1a9a46142d8b6df441ac8bd2f6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jul 2021 12:59:19 +0200 Subject: [PATCH 140/333] change settings retrieval --- .../plugins/publish/submit_maya_deadline.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 0e09641200..c0c39a52b6 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -272,18 +272,19 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): ) self._job_info = ( - context.data["project_settings"] - ["maya"] - ["publish"] - ["deadline"] - ["jobInfo"] + context.data["project_settings"].get( + "maya", {}).get( + "publish", {}).get( + "deadline", {}).get( + "jobInfo", {}) ) + self._plugin_info = ( - context.data["project_settings"] - ["maya"] - ["publish"] - ["deadline"] - ["pluginInfo"] + context.data["project_settings"].get( + "maya", {}).get( + "publish", {}).get( + "deadline", {}).get( + "pluginInfo", {}) ) assert self._deadline_url, "Requires DEADLINE_REST_URL" From 0551d76fb6ad74e38de35ada406e41c1acb614c7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jul 2021 13:16:38 +0200 Subject: [PATCH 141/333] move settings to deadline --- .../plugins/publish/submit_maya_deadline.py | 8 ++++---- .../defaults/project_settings/deadline.json | 14 ++++++++++---- .../defaults/project_settings/maya.json | 4 ---- .../schema_project_deadline.json | 10 ++++++++++ .../schemas/schema_maya_publish.json | 18 ------------------ 5 files changed, 24 insertions(+), 30 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index c0c39a52b6..5cb6dbbd88 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -273,17 +273,17 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self._job_info = ( context.data["project_settings"].get( - "maya", {}).get( - "publish", {}).get( "deadline", {}).get( + "publish", {}).get( + "MayaSubmitDeadline", {}).get( "jobInfo", {}) ) self._plugin_info = ( context.data["project_settings"].get( - "maya", {}).get( - "publish", {}).get( "deadline", {}).get( + "publish", {}).get( + "MayaSubmitDeadline", {}).get( "pluginInfo", {}) ) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 5861015f2c..2dba20d63c 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -3,9 +3,13 @@ "ValidateExpectedFiles": { "enabled": true, "active": true, - "families": ["render"], - "targets": ["deadline"], - "allow_user_override": true + "allow_user_override": true, + "families": [ + "render" + ], + "targets": [ + "deadline" + ] }, "MayaSubmitDeadline": { "enabled": true, @@ -15,7 +19,9 @@ "use_published": true, "asset_dependencies": true, "group": "none", - "limit": [] + "limit": [], + "jobInfo": {}, + "pluginInfo": {} }, "NukeSubmitDeadline": { "enabled": true, diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 62d8a74670..284a1a0040 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -391,10 +391,6 @@ } } }, - "deadline": { - "jobInfo": {}, - "pluginInfo": {} - }, "ExtractCameraAlembic": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 3281c9ce4d..27eeaef559 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -108,6 +108,16 @@ "key": "limit", "label": "Limit Groups", "object_type": "text" + }, + { + "type": "raw-json", + "key": "jobInfo", + "label": "Additional JobInfo data" + }, + { + "type": "raw-json", + "key": "pluginInfo", + "label": "Additional PluginInfo data" } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 7e50682f5d..5ca7059ee5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -309,24 +309,6 @@ "type": "schema", "name": "schema_maya_capture" }, - { - "type": "dict", - "collapsible": true, - "key": "deadline", - "label": "Additional Deadline Settings", - "children": [ - { - "type": "raw-json", - "key": "jobInfo", - "label": "Additional JobInfo data" - }, - { - "type": "raw-json", - "key": "pluginInfo", - "label": "Additional PluginInfo data" - } - ] - }, { "type": "dict", "collapsible": true, From 4d96cdc7200808c6accbb190e4ffea3f59a74cd6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jul 2021 13:34:19 +0200 Subject: [PATCH 142/333] fix check for Group defaults --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 5cb6dbbd88..a652da7786 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -423,7 +423,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.payload_skeleton["JobInfo"]["Priority"] = \ self._instance.data.get("priority", 50) - if self.group != "none": + if self.group != "none" and self.group: self.payload_skeleton["JobInfo"]["Group"] = self.group if self.limit_groups: From 08e8f6016193002979af8ed090d02634ca1c1db5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 15:04:28 +0200 Subject: [PATCH 143/333] list entity can use templates or schemas --- openpype/settings/entities/list_entity.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index 64bbad28a7..ce200862f6 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -141,7 +141,21 @@ class ListEntity(EndpointEntity): item_schema = self.schema_data["object_type"] if not isinstance(item_schema, dict): item_schema = {"type": item_schema} - self.item_schema = item_schema + + schema_template_used = False + _item_schemas = self.schema_hub.resolve_schema_data(item_schema) + if len(_item_schemas) == 1: + self.item_schema = _item_schemas[0] + if self.item_schema != item_schema: + schema_template_used = True + if "label" in self.item_schema: + self.item_schema.pop("label") + self.item_schema["use_label_wrap"] = False + else: + self.item_schema = _item_schemas + + # Store if was used template or schema + self._schema_template_used = schema_template_used if self.group_item is None: self.is_group = True From ff7ccfecba187237465151d1fbd6f5024973ef93 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 15:07:35 +0200 Subject: [PATCH 144/333] validate children on schema validations only if was not used from schema --- openpype/settings/entities/list_entity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index ce200862f6..4a2b5968d9 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -187,7 +187,11 @@ class ListEntity(EndpointEntity): child_validated = True break - if not child_validated: + # Do not validate if was used schema or template + # - that is validated on first created children + # - it is because template or schema can use itself inside children + # TODO Do validations maybe store to `schema_hub` what is validated + if not self._schema_template_used and not child_validated: idx = 0 tmp_child = self._add_new_item(idx) tmp_child.schema_validations() From 8b789df5ec7beaf19b8d473c1012949ff294fc84 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 15:07:58 +0200 Subject: [PATCH 145/333] validate if item_schema is list --- openpype/settings/entities/list_entity.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index 4a2b5968d9..b12e6d8f5c 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -164,6 +164,12 @@ class ListEntity(EndpointEntity): self.initial_value = [] def schema_validations(self): + if isinstance(self.item_schema, list): + reason = ( + "`ListWidget` has multiple items as object type." + ) + raise EntitySchemaError(self, reason) + super(ListEntity, self).schema_validations() if self.is_dynamic_item and self.use_label_wrap: From 8f6e8b19885e836a8adf3892659d282909f1cbf0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 15:08:09 +0200 Subject: [PATCH 146/333] handle child validations --- openpype/settings/entities/list_entity.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index b12e6d8f5c..e89c7cadec 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -94,6 +94,12 @@ class ListEntity(EndpointEntity): def _add_new_item(self, idx=None): child_obj = self.create_schema_object(self.item_schema, self, True) + + # Validate child if was not validated yet + if not self._child_validated: + child_obj.schema_validations() + self._child_validated = True + if idx is None: self.children.append(child_obj) else: @@ -156,6 +162,8 @@ class ListEntity(EndpointEntity): # Store if was used template or schema self._schema_template_used = schema_template_used + # Store if child was validated + self._child_validated = False if self.group_item is None: self.is_group = True @@ -202,6 +210,9 @@ class ListEntity(EndpointEntity): tmp_child = self._add_new_item(idx) tmp_child.schema_validations() self.children.pop(idx) + child_validated = True + + self._child_validated = child_validated def get_child_path(self, child_obj): result_idx = None From 0356e60faf83c48db881cf94ea21e6fa8a482b99 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 17:31:34 +0200 Subject: [PATCH 147/333] get template name from item --- openpype/settings/entities/lib.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index e58281644a..dee80c09aa 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -126,6 +126,22 @@ class SchemasHub: def gui_types(self): return self._gui_types + def get_template_name(self, item_def, default=None): + """Get template name from passed item definition. + + Args: + item_def(dict): Definition of item with "type". + default(object): Default return value. + """ + output = default + if not item_def or not isinstance(item_def, dict): + return output + + item_type = item_def.get("type") + if item_type in ("template", "schema_template"): + output = item_def["name"] + return output + def get_schema(self, schema_name): """Get schema definition data by it's name. From 41218b61ec79a5ad7dc68a42f636a44562c4cef5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 17:32:13 +0200 Subject: [PATCH 148/333] added validation methods and variables --- openpype/settings/entities/lib.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index dee80c09aa..1c4a51b7c9 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -111,6 +111,10 @@ class SchemasHub: self._loaded_templates = {} self._loaded_schemas = {} + # Store validating and validated dynamic template or schemas + self._validating_dynamic = set() + self._validated_dynamic = set() + # It doesn't make sence to reload types on each reset as they can't be # changed self._load_types() @@ -142,6 +146,27 @@ class SchemasHub: output = item_def["name"] return output + def is_dynamic_template_validating(self, template_name): + """Is template validating using different entity. + + Returns: + bool: Is template validating. + """ + if template_name in self._validating_dynamic: + return True + return False + + def is_dynamic_template_validated(self, template_name): + """Is template already validated. + + Returns: + bool: Is template validated. + """ + + if template_name in self._validated_dynamic: + return True + return False + def get_schema(self, schema_name): """Get schema definition data by it's name. From 027cb48a13e718dfcdb0c25327ddba4fbed677d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 17:36:54 +0200 Subject: [PATCH 149/333] added context manager method for using validation of dynamic template --- openpype/settings/entities/lib.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 1c4a51b7c9..01f61d8bdf 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -3,6 +3,7 @@ import re import json import copy import inspect +import contextlib from .exceptions import ( SchemaTemplateMissingKeys, @@ -167,6 +168,23 @@ class SchemasHub: return True return False + @contextlib.contextmanager + def validating_dynamic(self, template_name): + """Template name is validating and validated. + + Context manager that cares about storing template name validations of + template. + + This is to avoid infinite loop of dynamic children validation. + """ + self._validating_dynamic.add(template_name) + try: + yield + self._validated_dynamic.add(template_name) + + finally: + self._validating_dynamic.remove(template_name) + def get_schema(self, schema_name): """Get schema definition data by it's name. From 148e1a9564c421fabcd31349aac1a146db2c70e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 17:37:15 +0200 Subject: [PATCH 150/333] better children validation of dynamic templates --- openpype/settings/entities/list_entity.py | 42 ++++++++++++----------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index e89c7cadec..b07441251a 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -94,12 +94,6 @@ class ListEntity(EndpointEntity): def _add_new_item(self, idx=None): child_obj = self.create_schema_object(self.item_schema, self, True) - - # Validate child if was not validated yet - if not self._child_validated: - child_obj.schema_validations() - self._child_validated = True - if idx is None: self.children.append(child_obj) else: @@ -148,12 +142,11 @@ class ListEntity(EndpointEntity): if not isinstance(item_schema, dict): item_schema = {"type": item_schema} - schema_template_used = False + obj_template_name = self.schema_hub.get_template_name(item_schema) _item_schemas = self.schema_hub.resolve_schema_data(item_schema) if len(_item_schemas) == 1: self.item_schema = _item_schemas[0] if self.item_schema != item_schema: - schema_template_used = True if "label" in self.item_schema: self.item_schema.pop("label") self.item_schema["use_label_wrap"] = False @@ -161,9 +154,7 @@ class ListEntity(EndpointEntity): self.item_schema = _item_schemas # Store if was used template or schema - self._schema_template_used = schema_template_used - # Store if child was validated - self._child_validated = False + self._obj_template_name = obj_template_name if self.group_item is None: self.is_group = True @@ -195,24 +186,35 @@ class ListEntity(EndpointEntity): raise EntitySchemaError(self, reason) # Validate object type schema - child_validated = False + validate_children = True for child_entity in self.children: child_entity.schema_validations() - child_validated = True + validate_children = False break - # Do not validate if was used schema or template - # - that is validated on first created children - # - it is because template or schema can use itself inside children - # TODO Do validations maybe store to `schema_hub` what is validated - if not self._schema_template_used and not child_validated: + if validate_children and self._obj_template_name: + _validated = self.schema_hub.is_dynamic_template_validated( + self._obj_template_name + ) + _validating = self.schema_hub.is_dynamic_template_validating( + self._obj_template_name + ) + validate_children = not _validated and not _validating + + if not validate_children: + return + + def _validate(): idx = 0 tmp_child = self._add_new_item(idx) tmp_child.schema_validations() self.children.pop(idx) - child_validated = True - self._child_validated = child_validated + if self._obj_template_name: + with self.schema_hub.validating_dynamic(self._obj_template_name): + _validate() + else: + _validate() def get_child_path(self, child_obj): result_idx = None From 0eaf60f358598422cc6067444b5bf139584874cc Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 13 Jul 2021 11:54:36 +0000 Subject: [PATCH 151/333] [Automated] Release --- CHANGELOG.md | 7 +++---- openpype/version.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e76d7b76a..bc659bd629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.2.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...3.2.0) **🚀 Enhancements** @@ -11,6 +11,7 @@ - Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) - Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) - Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776) +- Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769) - Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766) - Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) @@ -24,7 +25,6 @@ **🐛 Bug fixes** - nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) -- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) - Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) - Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) - Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) @@ -105,7 +105,6 @@ - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) - Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) -- Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index dabeacc084..7bcd7face2 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.7" +__version__ = "3.2.0" From c8fe973155beb7323184ee6b31476257467b609f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:44:39 +0200 Subject: [PATCH 152/333] standalone: plugins prepare for settings --- .../publish/collect_editorial_instances.py | 17 +++++++----- .../publish/collect_editorial_resources.py | 20 ++++++++------ .../plugins/publish/collect_hierarchy.py | 27 ++++++++++--------- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index 3474cbcdde..dbf2574a9d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -17,16 +17,12 @@ class CollectInstances(pyblish.api.InstancePlugin): "referenceMain": { "family": "review", "families": ["clip"], - "extensions": [".mp4"] + "extensions": ["mp4"] }, "audioMain": { "family": "audio", "families": ["clip"], - "extensions": [".wav"], - }, - "shotMain": { - "family": "shot", - "families": [] + "extensions": ["wav"], } } timeline_frame_start = 900000 # starndard edl default (10:00:00:00) @@ -178,7 +174,16 @@ class CollectInstances(pyblish.api.InstancePlugin): data_key: instance.data.get(data_key)}) # adding subsets to context as instances + self.subsets.update({ + "shotMain": { + "family": "shot", + "families": [] + } + }) for subset, properities in self.subsets.items(): + if properities["version"] == 0: + properities.pop("version") + # adding Review-able instance subset_instance_data = instance_data.copy() subset_instance_data.update(properities) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py index e262009637..ffa24cfd93 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py @@ -177,19 +177,23 @@ class CollectInstanceResources(pyblish.api.InstancePlugin): collection_head_name = None # loop trough collections and create representations for _collection in collections: - ext = _collection.tail + ext = _collection.tail[1:] collection_head_name = _collection.head frame_start = list(_collection.indexes)[0] frame_end = list(_collection.indexes)[-1] repre_data = { "frameStart": frame_start, "frameEnd": frame_end, - "name": ext[1:], - "ext": ext[1:], + "name": ext, + "ext": ext, "files": [item for item in _collection], "stagingDir": staging_dir } + if instance_data.get("keepSequence"): + repre_data_keep = deepcopy(repre_data) + instance_data["representations"].append(repre_data_keep) + if "review" in instance_data["families"]: repre_data.update({ "thumbnail": True, @@ -208,20 +212,20 @@ class CollectInstanceResources(pyblish.api.InstancePlugin): # loop trough reminders and create representations for _reminding_file in remainder: - ext = os.path.splitext(_reminding_file)[-1] + ext = os.path.splitext(_reminding_file)[-1][1:] if ext not in instance_data["extensions"]: continue if collection_head_name and ( - (collection_head_name + ext[1:]) not in _reminding_file - ) and (ext in [".mp4", ".mov"]): + (collection_head_name + ext) not in _reminding_file + ) and (ext in ["mp4", "mov"]): self.log.info(f"Skipping file: {_reminding_file}") continue frame_start = 1 frame_end = 1 repre_data = { - "name": ext[1:], - "ext": ext[1:], + "name": ext, + "ext": ext, "files": _reminding_file, "stagingDir": staging_dir } diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py index be36f30f4b..ba2aed4bfc 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py @@ -131,20 +131,21 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin): tasks_to_add = dict() project_tasks = io.find_one({"type": "project"})["config"]["tasks"] for task_name, task_data in self.shot_add_tasks.items(): - try: - if task_data["type"] in project_tasks.keys(): - tasks_to_add.update({task_name: task_data}) - else: - raise KeyError( - "Wrong FtrackTaskType `{}` for `{}` is not" - " existing in `{}``".format( - task_data["type"], - task_name, - list(project_tasks.keys()))) - except KeyError as error: + _task_data = deepcopy(task_data) + + # fixing enumerator from settings + _task_data["type"] = task_data["type"][0] + + # check if task type in project task types + if _task_data["type"] in project_tasks.keys(): + tasks_to_add.update({task_name: _task_data}) + else: raise KeyError( - "Wrong presets: `{0}`".format(error) - ) + "Wrong FtrackTaskType `{}` for `{}` is not" + " existing in `{}``".format( + _task_data["type"], + task_name, + list(project_tasks.keys()))) instance.data["tasks"] = tasks_to_add else: From 7062096ef70d9589bae2ca7a4eb7243cdbf3b412 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:45:03 +0200 Subject: [PATCH 153/333] standalone: plugin settings --- .../project_settings/standalonepublisher.json | 45 ++++++ .../schema_project_standalonepublisher.json | 139 ++++++++++++++++++ 2 files changed, 184 insertions(+) diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 443203951d..52020f2ce8 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -165,6 +165,51 @@ ], "output": [] } + }, + "CollectHierarchyInstance": { + "shot_rename_template": "{project[code]}_{_sequence_}_{_shot_}", + "shot_rename_search_patterns": { + "_sequence_": "(\\d{4})(?=_\\d{4})", + "_shot_": "(\\d{4})(?!_\\d{4})" + }, + "shot_add_hierarchy": { + "parents_path": "{project}/{folder}/{sequence}", + "parents": { + "project": "{project[name]}", + "sequence": "{_sequence_}", + "folder": "shots" + } + }, + "shot_add_tasks": {} + }, + "shot_add_tasks": { + "custom_start_frame": 0, + "timeline_frame_start": 900000, + "timeline_frame_offset": 0, + "subsets": { + "referenceMain": { + "family": "review", + "families": [ + "clip" + ], + "extensions": [ + "mp4" + ], + "version": 0, + "keepSequence": false + }, + "audioMain": { + "family": "audio", + "families": [ + "clip" + ], + "extensions": [ + "wav" + ], + "version": 0, + "keepSequence": false + } + } } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 0ef7612805..30144341c2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -130,6 +130,145 @@ ] } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CollectHierarchyInstance", + "label": "Collect Instance Hierarchy", + "is_group": true, + "children": [ + { + "type": "text", + "key": "shot_rename_template", + "label": "Shot rename template" + }, + { + "key": "shot_rename_search_patterns", + "label": "Shot renaming paterns search", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "text" + } + }, + { + "type": "dict", + "key": "shot_add_hierarchy", + "label": "Shot hierarchy", + "children": [ + { + "type": "text", + "key": "parents_path", + "label": "Parents path template" + }, + { + "key": "parents", + "label": "Parents", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "text" + } + } + ] + }, + { + "key": "shot_add_tasks", + "label": "Add tasks to shot", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "task-types-enum", + "key": "type", + "label": "Task type" + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "shot_add_tasks", + "label": "Collect Clip Instances", + "is_group": true, + "children": [ + { + "type": "number", + "key": "custom_start_frame", + "label": "Custom start frame", + "default": 0, + "minimum": 1, + "maximum": 100000 + }, + { + "type": "number", + "key": "timeline_frame_start", + "label": "Timeline start frame", + "default": 900000, + "minimum": 1, + "maximum": 10000000 + }, + { + "type": "number", + "key": "timeline_frame_offset", + "label": "Timeline frame offset", + "default": 0, + "minimum": -1000000, + "maximum": 1000000 + }, + { + "key": "subsets", + "label": "Subsets", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "family", + "label": "Family" + }, + { + "type": "list", + "key": "families", + "label": "Families", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "type": "list", + "key": "extensions", + "label": "Extensions", + "object_type": "text" + }, + { + "key": "version", + "label": "Version lock", + "type": "number", + "default": 0, + "minimum": 0, + "maximum": 10 + } + , + { + "type": "boolean", + "key": "keepSequence", + "label": "Keep sequence if used for review", + "default": false + } + ] + } + } + ] } ] } From 3deef5b0ded90dba2d3b268dacec4c2294811100 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:45:22 +0200 Subject: [PATCH 154/333] settings: adding `standalonepublisher` to hosts --- openpype/settings/entities/enum_entity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 63e0afeb47..d306eca7ef 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -139,7 +139,8 @@ class HostsEnumEntity(BaseEnumEntity): "photoshop", "resolve", "tvpaint", - "unreal" + "unreal", + "standalonepublisher" ] if self.use_empty_value: host_names.insert(0, "") From 5093360e549e01a19696d27ed407ccec920ba2d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:58:29 +0200 Subject: [PATCH 155/333] standalone: prepare editorial plugin --- .../plugins/publish/collect_editorial.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py index fc9d95d3d7..5d61cb7f43 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py @@ -2,7 +2,7 @@ Optional: presets -> extensions ( example of use: - [".mov", ".mp4"] + ["mov", "mp4"] ) presets -> source_dir ( example of use: @@ -11,6 +11,7 @@ Optional: "{root[work]}/{project[name]}/inputs" "./input" "../input" + "" ) """ @@ -48,7 +49,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): actions = [] # presets - extensions = [".mov", ".mp4"] + extensions = ["mov", "mp4"] source_dir = None def process(self, instance): @@ -72,7 +73,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): video_path = None basename = os.path.splitext(os.path.basename(file_path))[0] - if self.source_dir: + if self.source_dir is not "": source_dir = self.source_dir.replace("\\", "/") if ("./" in source_dir) or ("../" in source_dir): # get current working dir @@ -98,7 +99,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): if os.path.splitext(f)[0] not in basename: continue # filter out by respected extensions - if os.path.splitext(f)[1] not in self.extensions: + if os.path.splitext(f)[1][1:] not in self.extensions: continue video_path = os.path.join( staging_dir, f From fa6148e3f0d0719f061c84a9ebf79cd73ebc83a0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:59:07 +0200 Subject: [PATCH 156/333] standalone: settings for editorial plugin --- .../project_settings/standalonepublisher.json | 7 +++++++ .../schema_project_standalonepublisher.json | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 52020f2ce8..f08212934d 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -166,6 +166,13 @@ "output": [] } }, + "CollectEditorial": { + "source_dir": "", + "extensions": [ + "mov", + "mp4" + ] + }, "CollectHierarchyInstance": { "shot_rename_template": "{project[code]}_{_sequence_}_{_shot_}", "shot_rename_search_patterns": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 30144341c2..c627012531 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -131,6 +131,26 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectEditorial", + "label": "Collect Editorial", + "is_group": true, + "children": [ + { + "type": "text", + "key": "source_dir", + "label": "Editorial resources pointer" + }, + { + "type": "list", + "key": "extensions", + "label": "Accepted extensions", + "object_type": "text" + } + ] + }, { "type": "dict", "collapsible": true, From a8858fa149493b7b052c3400fb1fd94a2d18b59a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 17:08:13 +0200 Subject: [PATCH 157/333] hound: suggestions --- .../standalonepublisher/plugins/publish/collect_editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py index 5d61cb7f43..0a1d29ccdc 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py @@ -73,7 +73,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): video_path = None basename = os.path.splitext(os.path.basename(file_path))[0] - if self.source_dir is not "": + if self.source_dir != "": source_dir = self.source_dir.replace("\\", "/") if ("./" in source_dir) or ("../" in source_dir): # get current working dir From 1886f29a69dd07b84561c6b7478b503f711ef731 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 17:26:58 +0200 Subject: [PATCH 158/333] settings: global CleanUp --- .../defaults/project_settings/global.json | 4 ++++ .../schemas/schema_global_publish.json | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 037fa63a29..94d2b3bb3a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -184,6 +184,10 @@ ".*" ] } + }, + "CleanUp": { + "paterns": [], + "remove_temp_renders": false } }, "tools": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 496635287f..5c6cbe0e6f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -594,6 +594,30 @@ ] } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CleanUp", + "label": "Clean Up", + "is_group": true, + "children": [ + { + "type": "list", + "key": "paterns", + "label": "Paterrns (regex)", + "object_type": { + "type": "text" + } + }, + { + "type": "boolean", + "key": "remove_temp_renders", + "label": "Remove Temp renders", + "default": false + } + + ] } ] } From 66fbd2f4a36bc75e0a9de99f4789260ed3e31222 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 17:48:54 +0200 Subject: [PATCH 159/333] settings: updating ProcessSubmittedJobOnFarm plugin --- .../defaults/project_settings/global.json | 2 ++ .../schemas/schema_global_publish.json | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 94d2b3bb3a..826b5ab465 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -172,6 +172,8 @@ "deadline_group": "", "deadline_chunk_size": 1, "deadline_priority": 50, + "publishing_script": "", + "skip_integration_repre_list": [], "aov_filter": { "maya": [ ".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5c6cbe0e6f..4715db4888 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -554,6 +554,22 @@ "key": "deadline_priority", "label": "Deadline Priotity" }, + { + "type": "splitter" + }, + { + "type": "text", + "key": "publishing_script", + "label": "Publishing script path" + }, + { + "type": "list", + "key": "skip_integration_repre_list", + "label": "Skip integration of representation with ext", + "object_type": { + "type": "text" + } + }, { "type": "dict", "key": "aov_filter", From abd02b945c71249d5ebd98b35bee2916a7b6a6fb Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 14 Jul 2021 03:42:08 +0000 Subject: [PATCH 160/333] [Automated] Bump version --- CHANGELOG.md | 17 +++++++++++++++-- openpype/version.py | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc659bd629..0ed0159a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog +## [3.3.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) + +**🐛 Bug fixes** + +- Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) + +**Merged pull requests:** + +- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) + ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...3.2.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0) **🚀 Enhancements** @@ -25,6 +37,7 @@ **🐛 Bug fixes** - nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) +- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) - Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) - Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) - Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) @@ -69,7 +82,7 @@ - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) -**Merged pull requests:** +**⚠️ Deprecations** - global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) diff --git a/openpype/version.py b/openpype/version.py index 7bcd7face2..2fc2b4bc26 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0" +__version__ = "3.3.0-nightly.1" From 570aa35269c37c3291985e9e38ad7cab57f986f3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 14 Jul 2021 11:55:00 +0200 Subject: [PATCH 161/333] add support for pyenv-win on windows --- tools/build.ps1 | 6 +++++- tools/create_env.ps1 | 14 +++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index cc4253fe24..e1962ee933 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -83,8 +83,12 @@ function Show-PSWarning() { function Install-Poetry() { Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Installing Poetry ... " + $python = "python" + if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + $python = & pyenv which python + } $env:POETRY_HOME="$openpype_root\.poetry" - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - + (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | & $($python) - } $art = @" diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index 6c8124ccb2..2ab6abe76e 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -48,15 +48,23 @@ function Show-PSWarning() { function Install-Poetry() { Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Installing Poetry ... " + $python = "python" + if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + $python = & pyenv which python + } $env:POETRY_HOME="$openpype_root\.poetry" - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - + (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | & $($python) - } function Test-Python() { Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Detecting host Python ... " -NoNewline - if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) { + $python = "python" + if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + $python = & pyenv which python + } + if (-not (Get-Command "python3" -ErrorAction SilentlyContinue)) { Write-Host "!!! Python not detected" -ForegroundColor red Set-Location -Path $current_dir Exit-WithCode 1 @@ -66,7 +74,7 @@ import sys print('{0}.{1}'.format(sys.version_info[0], sys.version_info[1])) '@ - $p = & python -c $version_command + $p = & $python -c $version_command $env:PYTHON_VERSION = $p $m = $p -match '(\d+)\.(\d+)' if(-not $m) { From ae13ab38eb48814a629d47efec5cfb7ff65bc028 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 14 Jul 2021 14:19:55 +0200 Subject: [PATCH 162/333] add support to select preferred mongo version on windows --- tools/run_mongo.ps1 | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/tools/run_mongo.ps1 b/tools/run_mongo.ps1 index 6719e520fe..32f6cfed17 100644 --- a/tools/run_mongo.ps1 +++ b/tools/run_mongo.ps1 @@ -41,22 +41,40 @@ function Exit-WithCode($exitcode) { } -function Find-Mongo { +function Find-Mongo ($preferred_version) { $defaultPath = "C:\Program Files\MongoDB\Server" Write-Host ">>> " -NoNewLine -ForegroundColor Green Write-Host "Detecting MongoDB ... " -NoNewline if (-not (Get-Command "mongod" -ErrorAction SilentlyContinue)) { if(Test-Path "$($defaultPath)\*\bin\mongod.exe" -PathType Leaf) { # we have mongo server installed on standard Windows location - # so we can inject it to the PATH. We'll use latest version available. + # so we can inject it to the PATH. We'll use latest version available, or the one defined by + # $preferred_version. $mongoVersions = Get-ChildItem -Directory 'C:\Program Files\MongoDB\Server' | Sort-Object -Property {$_.Name -as [int]} if(Test-Path "$($mongoVersions[-1])\bin\mongod.exe" -PathType Leaf) { - $env:PATH = "$($env:PATH);$($mongoVersions[-1])\bin\" Write-Host "OK" -ForegroundColor Green + $use_version = $mongoVersions[-1] + foreach ($v in $mongoVersions) { + Write-Host " - found [ " -NoNewline + Write-Host $v -NoNewLine -ForegroundColor Cyan + Write-Host " ]" -NoNewLine + + $version = Split-Path $v -Leaf + + if ($preferred_version -eq $version) { + Write-Host " *" -ForegroundColor Green + $use_version = $v + } else { + Write-Host "" + } + } + + $env:PATH = "$($env:PATH);$($use_version)\bin\" + Write-Host " - auto-added from [ " -NoNewline - Write-Host "$($mongoVersions[-1])\bin\mongod.exe" -NoNewLine -ForegroundColor Cyan + Write-Host "$($use_version)\bin\mongod.exe" -NoNewLine -ForegroundColor Cyan Write-Host " ]" - return "$($mongoVersions[-1])\bin\mongod.exe" + return "$($use_version)\bin\mongod.exe" } else { Write-Host "FAILED " -NoNewLine -ForegroundColor Red Write-Host "MongoDB not detected" -ForegroundColor Yellow @@ -95,7 +113,18 @@ $port = 2707 # path to database $dbpath = (Get-Item $openpype_root).parent.FullName + "\mongo_db_data" -$mongoPath = Find-Mongo -Start-Process -FilePath $mongopath "--dbpath $($dbpath) --port $($port)" -PassThru +$preferred_version = "4.0" +$mongoPath = Find-Mongo $preferred_version +Write-Host ">>> " -NoNewLine -ForegroundColor Green +Write-Host "Using DB path: " -NoNewLine +Write-Host " [ " -NoNewline -ForegroundColor Cyan +Write-Host "$($dbpath)" -NoNewline -ForegroundColor White +Write-Host " ] "-ForegroundColor Cyan +Write-Host ">>> " -NoNewLine -ForegroundColor Green +Write-Host "Port: " -NoNewLine +Write-Host " [ " -NoNewline -ForegroundColor Cyan +Write-Host "$($port)" -NoNewline -ForegroundColor White +Write-Host " ] " -ForegroundColor Cyan +Start-Process -FilePath $mongopath "--dbpath $($dbpath) --port $($port)" -PassThru | Out-Null From 7ab243cd29a400bdcde2e16269ad1b595ebe6db6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 15 Jul 2021 13:27:17 +0200 Subject: [PATCH 163/333] update acre in poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 30dbe50c19..aad1898983 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ develop = false type = "git" url = "https://github.com/pypeclub/acre.git" reference = "master" -resolved_reference = "68784b7eb5b7bb5f409b61ab31d4403878a3e1b7" +resolved_reference = "5a812c6dcfd3aada87adb49be98c548c894d6566" [[package]] name = "aiohttp" From 858e46d0f63e3e2e2575935f9e4a673d6932b7af Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 15 Jul 2021 13:53:58 +0200 Subject: [PATCH 164/333] added description and example --- openpype/settings/entities/schemas/README.md | 59 ++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 3c360b892f..e098198c2c 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -404,6 +404,8 @@ How output of the schema could look like on save: - there are 2 possible ways how to set the type: 1.) dictionary with item modifiers (`number` input has `minimum`, `maximum` and `decimals`) in that case item type must be set as value of `"type"` (example below) 2.) item type name as string without modifiers (e.g. `text`) + 3.) enhancement of 1.) there is also support of `template` type but be carefull about endless loop of templates + - goal of using `template` is to easily change same item definitions in multiple lists 1.) with item modifiers ``` @@ -429,6 +431,63 @@ How output of the schema could look like on save: } ``` +3.) with template definition +``` +# Schema of list item where template is used +{ + "type": "list", + "key": "menu_items", + "label": "Menu Items", + "object_type": { + "type": "template", + "name": "template_object_example" + } +} + +# WARNING: +# In this example the template use itself inside which will work in `list` +# but may cause an issue in other entity types (e.g. `dict`). +[ + { + "type": "dict-conditional", + "use_label_wrap": true, + "collapsible": true, + "key": "menu_items", + "label": "Menu items", + "enum_key": "type", + "enum_label": "Type", + "enum_children": [ + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "key": "children", + "label": "Children", + "type": "list", + "object_type": { + "type": "template", + "name": "template_object_example" + } + } + ] + } + ] + } +] +``` + ### dict-modifiable - one of dictionary inputs, this is only used as value input - items in this input can be removed and added same way as in `list` input From 1b6b3fe859ccf4c3f3f9deb5de7d852d8842ea2a Mon Sep 17 00:00:00 2001 From: jezscha Date: Thu, 15 Jul 2021 13:15:32 +0000 Subject: [PATCH 165/333] Create draft PR for #1828 From a62607ed7161388a3110bdf515762298d881623e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 15 Jul 2021 17:30:59 +0200 Subject: [PATCH 166/333] Nuke: settings create write with default subset names --- .../settings/defaults/project_settings/nuke.json | 15 +++++++++++++-- .../projects_schema/schema_project_nuke.json | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 71bf46d5b3..136f1d6b42 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -10,11 +10,22 @@ }, "create": { "CreateWriteRender": { - "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}" + "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}", + "defaults": [ + "Main", + "Mask" + ] }, "CreateWritePrerender": { "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}", - "use_range_limit": true + "use_range_limit": true, + "defaults": [ + "Key01", + "Bg01", + "Fg01", + "Branch01", + "Part01" + ] } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 01a954f283..e0b21f4037 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -63,6 +63,14 @@ "type": "text", "key": "fpath_template", "label": "Path template" + }, + { + "type": "list", + "key": "defaults", + "label": "Subset name defaults", + "object_type": { + "type": "text" + } } ] }, @@ -82,6 +90,14 @@ "type": "boolean", "key": "use_range_limit", "label": "Use Frame range limit by default" + }, + { + "type": "list", + "key": "defaults", + "label": "Subset name defaults", + "object_type": { + "type": "text" + } } ] } From 924300324666c3d620ab25250846af03042d5869 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 17 Jul 2021 03:41:20 +0000 Subject: [PATCH 167/333] [Automated] Bump version --- CHANGELOG.md | 17 +++++++++-------- openpype/version.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed0159a4d..467ed7c0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,21 @@ # Changelog -## [3.3.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) +**🚀 Enhancements** + +- nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829) +- Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) +- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) + **🐛 Bug fixes** +- Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) +- Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) -**Merged pull requests:** - -- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) - ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0) @@ -55,8 +59,6 @@ - StandalonePublisher: failing collector for editorial [\#1738](https://github.com/pypeclub/OpenPype/pull/1738) - Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) - TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735) -- Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) -- Unreal: launching on Linux [\#1672](https://github.com/pypeclub/OpenPype/pull/1672) **Merged pull requests:** @@ -117,7 +119,6 @@ - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) - Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) -- Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 2fc2b4bc26..00df9438eb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.1" +__version__ = "3.3.0-nightly.2" From 170b63ff1404a216337ee6a801b9492fc6796b9c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Jul 2021 10:54:02 +0200 Subject: [PATCH 168/333] Textures - added multiple validations --- .../plugins/publish/collect_texture.py | 4 +-- .../publish/extract_workfile_location.py | 3 +- .../publish/validate_texture_has_workfile.py | 20 +++++++++++ .../publish/validate_texture_versions.py | 36 +++++++++++++------ .../publish/validate_texture_workfiles.py | 9 +++-- 5 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index b8f8f05dc9..5a418dd8da 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -245,7 +245,7 @@ class CollectTextures(pyblish.api.ContextPlugin): } ) - workfile = workfile_files.get(asset_build, "DUMMY") + workfile = workfile_files.get(asset_build) if resource_files.get(subset): # add resources only when workfile is main style @@ -266,7 +266,7 @@ class CollectTextures(pyblish.api.ContextPlugin): repre = representations.get(subset)[0] new_instance.context.data["currentFile"] = os.path.join( - repre["stagingDir"], workfile) + repre["stagingDir"], workfile or 'dummy.txt') new_instance.data["families"] = families diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py index 4345cef6dc..f91851c201 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py @@ -33,7 +33,8 @@ class ExtractWorkfileUrl(pyblish.api.ContextPlugin): filepath)) if not filepath: - raise ValueError("Texture batch doesn't contain workfile.") + self.log.info("Texture batch doesn't contain workfile.") + return # then apply to all textures for instance in context: diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py new file mode 100644 index 0000000000..7cd540668c --- /dev/null +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_has_workfile.py @@ -0,0 +1,20 @@ +import pyblish.api +import openpype.api + + +class ValidateTextureHasWorkfile(pyblish.api.InstancePlugin): + """Validates that textures have appropriate workfile attached. + + Workfile is optional, disable this Validator after Refresh if you are + sure it is not needed. + """ + label = "Validate Texture Has Workfile" + hosts = ["standalonepublisher"] + order = openpype.api.ValidateContentsOrder + families = ["textures"] + optional = True + + def process(self, instance): + wfile = instance.data["versionData"].get("workfile") + + assert wfile, "Textures are missing attached workfile" diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py index 3985cb8933..426151e390 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py @@ -3,22 +3,36 @@ import openpype.api class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): - """Validates that versions match in workfile and textures.""" + """Validates that versions match in workfile and textures. + + Workfile is optional, so if you are sure, you can disable this + validator after Refresh. + + Validates that only single version is published at a time. + """ label = "Validate Texture Batch Versions" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder families = ["textures"] - optional = True + optional = False def process(self, instance): - wfile = instance.data["versionData"]["workfile"] + wfile = instance.data["versionData"].get("workfile") version_str = "v{:03d}".format(instance.data["version"]) - if 'DUMMY' in wfile: - self.log.warning("Textures are missing attached workfile") - else: - msg = "Not matching version: texture v{:03d} - workfile {}" - assert version_str in wfile, \ - msg.format( - instance.data["version"], wfile - ) + + if not wfile: # no matching workfile, do not check versions + self.log.info("No workfile present for textures") + return + + msg = "Not matching version: texture v{:03d} - workfile {}" + assert version_str in wfile, \ + msg.format( + instance.data["version"], wfile + ) + + present_versions = [] + for instance in instance.context: + present_versions.append(instance.data["version"]) + + assert len(present_versions) == 1, "Too many versions in a batch!" diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py index 556a73dc4f..189246144d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -8,7 +8,7 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): Collected recourses means secondary workfiles (in most cases). """ - label = "Validate Texture Workfile" + label = "Validate Texture Workfile Has Resources" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder families = ["workfile"] @@ -16,7 +16,6 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): def process(self, instance): if instance.data["family"] == "workfile": - if not instance.data.get("resources"): - msg = "No resources for workfile {}".\ - format(instance.data["name"]) - self.log.warning(msg) + msg = "No resources for workfile {}".\ + format(instance.data["name"]) + assert instance.data.get("resources"), msg From ae2dfc66f17ddc6d4a893037c9f497b4b88d6777 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Jul 2021 13:01:12 +0200 Subject: [PATCH 169/333] Textures - settings schema + defaults --- .../project_settings/standalonepublisher.json | 54 +++++++++ .../schema_project_standalonepublisher.json | 113 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 5590fa6349..37807983a8 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -149,6 +149,60 @@ } }, "publish": { + "CollectTextures": { + "enabled": true, + "active": true, + "main_workfile_extensions": [ + "mra" + ], + "other_workfile_extensions": [ + "spp", + "psd" + ], + "texture_extensions": [ + "exr", + "dpx", + "jpg", + "jpeg", + "png", + "tiff", + "tga", + "gif", + "svg" + ], + "workfile_families": [], + "texture_families": [], + "color_space": [ + "linsRGB", + "raw", + "acesg" + ], + "input_naming_patterns": { + "workfile": [ + "^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+" + ], + "textures": [ + "^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+" + ] + }, + "input_naming_groups": { + "workfile": [ + "asset", + "filler", + "version" + ], + "textures": [ + "asset", + "shader", + "version", + "channel", + "color_space", + "udim" + ] + }, + "workfile_subset_template": "textures{Subset}Workfile", + "texture_subset_template": "textures{Subset}_{Shader}_{Channel}" + }, "ValidateSceneSettings": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 0ef7612805..41e6360a86 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -56,6 +56,119 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectTextures", + "label": "Collect Textures", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "list", + "key": "main_workfile_extensions", + "object_type": "text", + "label": "Main workfile extensions" + }, + { + "key": "other_workfile_extensions", + "label": "Support workfile extensions", + "type": "list", + "object_type": "text" + }, + { + "type": "list", + "key": "texture_extensions", + "object_type": "text", + "label": "Texture extensions" + }, + { + "type": "list", + "key": "workfile_families", + "object_type": "text", + "label": "Additional families for workfile" + }, + { + "type": "list", + "key": "texture_families", + "object_type": "text", + "label": "Additional families for textures" + }, + { + "type": "list", + "key": "color_space", + "object_type": "text", + "label": "Color spaces" + }, + { + "type": "dict", + "collapsible": false, + "key": "input_naming_patterns", + "label": "Regex patterns for naming conventions", + "children": [ + { + "type": "label", + "label": "Add regex groups matching expected name" + }, + { + "type": "list", + "object_type": "text", + "key": "workfile", + "label": "Workfile naming pattern" + }, + { + "type": "list", + "object_type": "text", + "key": "textures", + "label": "Textures naming pattern" + } + ] + }, + { + "type": "dict", + "collapsible": false, + "key": "input_naming_groups", + "label": "Group order for regex patterns", + "children": [ + { + "type": "label", + "label": "Add names of matched groups in correct order. Available values: ('filler', 'asset', 'shader', 'version', 'channel', 'color_space', 'udim')" + }, + { + "type": "list", + "object_type": "text", + "key": "workfile", + "label": "Workfile group positions" + }, + { + "type": "list", + "object_type": "text", + "key": "textures", + "label": "Textures group positions" + } + ] + }, + { + "type": "text", + "key": "workfile_subset_template", + "label": "Subset name template for workfile" + }, + { + "type": "text", + "key": "texture_subset_template", + "label": "Subset name template for textures" + } + ] + }, { "type": "dict", "collapsible": true, From 218522338c057e8087e6a3090f136c17fab97701 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Jul 2021 13:02:53 +0200 Subject: [PATCH 170/333] Textures - changes because of settings --- .../plugins/publish/collect_texture.py | 59 +++++++++++-------- .../publish/validate_texture_versions.py | 4 +- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 5a418dd8da..0fa554aa8b 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -5,6 +5,8 @@ import json from avalon.api import format_template_with_optional_keys +from openpype.lib import prepare_template_data + class CollectTextures(pyblish.api.ContextPlugin): """Collect workfile (and its resource_files) and textures. @@ -44,18 +46,19 @@ class CollectTextures(pyblish.api.ContextPlugin): input_naming_patterns = { # workfile: corridorMain_v001.mra > # texture: corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr - r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+': - r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', + "workfile": r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+', + "textures": r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', } # matching regex group position to 'input_naming_patterns' input_naming_groups = { - ('asset', 'filler', 'version'): - ('asset', 'shader', 'version', 'channel', 'color_space', 'udim') + "workfile": ('asset', 'filler', 'version'), + "textures": ('asset', 'shader', 'version', 'channel', 'color_space', + 'udim') } - workfile_subset_template = "textures{}Workfile" + workfile_subset_template = "textures{Subset}Workfile" # implemented keys: ["color_space", "channel", "subset", "shader"] - texture_subset_template = "textures{subset}_{shader}_{channel}" + texture_subset_template = "textures{Subset}_{Shader}_{Channel}" def process(self, context): self.context = context @@ -77,8 +80,14 @@ class CollectTextures(pyblish.api.ContextPlugin): parsed_subset = instance.data["subset"].replace( instance.data["family"], '') - workfile_subset = self.workfile_subset_template.format( - parsed_subset) + + fill_pairs = { + "subset": parsed_subset + } + + fill_pairs = prepare_template_data(fill_pairs) + workfile_subset = format_template_with_optional_keys( + fill_pairs, self.workfile_subset_template) processed_instance = False for repre in instance.data["representations"]: @@ -95,14 +104,14 @@ class CollectTextures(pyblish.api.ContextPlugin): asset_build = self._get_asset_build( repre_file, - self.input_naming_patterns.keys(), - self.input_naming_groups.keys(), + self.input_naming_patterns["workfile"], + self.input_naming_groups["workfile"], self.color_space ) version = self._get_version( repre_file, - self.input_naming_patterns.keys(), - self.input_naming_groups.keys(), + self.input_naming_patterns["workfile"], + self.input_naming_groups["workfile"], self.color_space ) asset_builds.add((asset_build, version, @@ -146,15 +155,15 @@ class CollectTextures(pyblish.api.ContextPlugin): channel = self._get_channel_name( repre_file, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space ) shader = self._get_shader_name( repre_file, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space ) @@ -164,19 +173,21 @@ class CollectTextures(pyblish.api.ContextPlugin): "shader": shader, "subset": parsed_subset } + + fill_pairs = prepare_template_data(formatting_data) subset = format_template_with_optional_keys( - formatting_data, self.texture_subset_template) + fill_pairs, self.texture_subset_template) asset_build = self._get_asset_build( repre_file, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space ) version = self._get_version( repre_file, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space ) if not representations.get(subset): @@ -404,7 +415,7 @@ class CollectTextures(pyblish.api.ContextPlugin): pattern = input_pattern.replace('{color_space}', cs) regex_result = re.findall(pattern, name) if regex_result: - idx = list(input_naming_groups)[0].index(key) + idx = list(input_naming_groups).index(key) if idx < 0: msg = "input_naming_groups must " +\ "have '{}' key".format(key) @@ -431,8 +442,8 @@ class CollectTextures(pyblish.api.ContextPlugin): for file_name in files: udim = self._get_udim(file_name, - self.input_naming_patterns.values(), - self.input_naming_groups.values(), + self.input_naming_patterns["textures"], + self.input_naming_groups["textures"], self.color_space) udims.append(udim) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py index 426151e390..90d0e8e512 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_versions.py @@ -31,8 +31,8 @@ class ValidateTextureBatchVersions(pyblish.api.InstancePlugin): instance.data["version"], wfile ) - present_versions = [] + present_versions = set() for instance in instance.context: - present_versions.append(instance.data["version"]) + present_versions.add(instance.data["version"]) assert len(present_versions) == 1, "Too many versions in a batch!" From bf1948b354791cbed390408fae1ffcc983f696a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 19 Jul 2021 14:02:36 +0200 Subject: [PATCH 171/333] Textures - added documentation --- .../assets/standalone_creators.png | Bin 0 -> 13991 bytes .../settings_project_standalone.md | 81 ++++++++++++++++++ website/sidebars.js | 3 +- 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 website/docs/project_settings/assets/standalone_creators.png create mode 100644 website/docs/project_settings/settings_project_standalone.md diff --git a/website/docs/project_settings/assets/standalone_creators.png b/website/docs/project_settings/assets/standalone_creators.png new file mode 100644 index 0000000000000000000000000000000000000000..cfadfa305da0af097bb729015aca8896a7b6cc34 GIT binary patch literal 13991 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 literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_standalone.md b/website/docs/project_settings/settings_project_standalone.md new file mode 100644 index 0000000000..5180486d29 --- /dev/null +++ b/website/docs/project_settings/settings_project_standalone.md @@ -0,0 +1,81 @@ +--- +id: settings_project_standalone +title: Project Standalone Publisher Setting +sidebar_label: Standalone Publisher +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Project settings can have project specific values. Each new project is using studio values defined in **default** project but these values can be modified or overriden per project. + +:::warning Default studio values +Projects always use default project values unless they have [project override](../admin_settings#project-overrides) (orage colour). Any changes in default project may affect all existing projects. +::: + +## Creator Plugins + +Contains list of implemented families to show in middle menu in Standalone Publisher. Each plugin must contain: +- name +- label +- family +- icon +- default subset(s) +- help (additional short information about family) + +![example of creator plugin](assets/standalone_creators.png) + +## Publish plugins + +### Collect Textures + +Serves to collect all needed information about workfiles and textures created from those. Allows to publish +main workfile (for example from Mari), additional worfiles (from Substance Painter) and exported textures. + +Available configuration: +- Main workfile extension - only single workfile can be "main" one +- Support workfile extensions - additional workfiles will be published to same folder as "main", just under `resourses` subfolder +- Texture extension - what kind of formats are expected for textures +- Additional families for workfile - should any family ('ftrack', 'review') be added to published workfile +- Additional families for textures - should any family ('ftrack', 'review') be added to published textures + +#### Naming conventions + +Implementation tries to be flexible and cover multiple naming conventions for workfiles and textures. + +##### Workfile naming pattern + +Provide regex matching pattern containing regex groups used to parse workfile name to learn needed information. (For example +build name.) + +Example: +```^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+``` - parses `corridorMain_v001` into three groups: +- asset build (`corridorMain`) +- filler (in this case empty) +- version (`001`) + +In case of different naming pattern, additional groups could be added or removed. + +##### Workfile group positions + +For each matching regex group set in previous paragraph, its ordinal position is required (in case of need for addition of new groups etc.) +Number of groups added here must match number of parsing groups from `Workfile naming pattern`. + +Same configuration is available for texture files. + +##### Output names + +Output names of published workfiles and textures could be configured separately: +- Subset name template for workfile +- Subset name template for textures (implemented keys: ["color_space", "channel", "subset", "shader"]) + + +### Validate Scene Settings + +#### Check Frame Range for Extensions + +Configure families, file extension and task to validate that DB setting (frame range) matches currently published values. + +### ExtractThumbnailSP + +Plugin responsible for generating thumbnails, configure appropriate values for your version o ffmpeg. \ No newline at end of file diff --git a/website/sidebars.js b/website/sidebars.js index d38973e40f..488814a385 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -65,7 +65,8 @@ module.exports = { label: "Project Settings", items: [ "project_settings/settings_project_global", - "project_settings/settings_project_nuke" + "project_settings/settings_project_nuke", + "project_settings/settings_project_standalone" ], }, ], From 38cf278b7fd9db97e8a04b9a0340d16fd5f511c0 Mon Sep 17 00:00:00 2001 From: jezscha Date: Mon, 19 Jul 2021 13:22:58 +0000 Subject: [PATCH 172/333] Create draft PR for #1835 From aee7ed3da1f5811a3f5387c05f7241f297cd310c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 20 Jul 2021 08:54:11 +0200 Subject: [PATCH 173/333] nuke: fix write node name not Crop01 --- openpype/hosts/nuke/api/lib.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index d7f3fdc6ba..ee03e04360 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -391,13 +391,14 @@ def create_write_node(name, data, input=None, prenodes=None, if prenodes: for node in prenodes: # get attributes - name = node["name"] + pre_node_name = node["name"] klass = node["class"] knobs = node["knobs"] dependent = node["dependent"] # create node - now_node = nuke.createNode(klass, "name {}".format(name)) + now_node = nuke.createNode( + klass, "name {}".format(pre_node_name)) now_node.hideControlPanel() # add data to knob @@ -476,27 +477,27 @@ def create_write_node(name, data, input=None, prenodes=None, linked_knob_names.append("Render") - for name in linked_knob_names: - if "_grp-start_" in name: + for _k_name in linked_knob_names: + if "_grp-start_" in _k_name: knob = nuke.Tab_Knob( "rnd_attr", "Rendering attributes", nuke.TABBEGINCLOSEDGROUP) GN.addKnob(knob) - elif "_grp-end_" in name: + elif "_grp-end_" in _k_name: knob = nuke.Tab_Knob( "rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP) GN.addKnob(knob) else: - if "___" in name: + if "___" in _k_name: # add devider GN.addKnob(nuke.Text_Knob("")) else: - # add linked knob by name + # add linked knob by _k_name link = nuke.Link_Knob("") - link.makeLink(write_node.name(), name) - link.setName(name) + link.makeLink(write_node.name(), _k_name) + link.setName(_k_name) # make render - if "Render" in name: + if "Render" in _k_name: link.setLabel("Render Local") link.setFlag(0x1000) GN.addKnob(link) From cb8aa03b64cf41e1289a6ed6d25d2143363f7b71 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 20 Jul 2021 11:19:16 +0200 Subject: [PATCH 174/333] Textures - fix - multiple version loaded at same time fails in better spot --- .../plugins/publish/collect_texture.py | 8 ++++++-- .../tools/standalonepublish/widgets/widget_drop_frame.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 0fa554aa8b..439168ea10 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -421,8 +421,12 @@ class CollectTextures(pyblish.api.ContextPlugin): "have '{}' key".format(key) raise ValueError(msg) - parsed_value = regex_result[0][idx] - return parsed_value + try: + parsed_value = regex_result[0][idx] + return parsed_value + except IndexError: + self.log.warning("Wrong index, probably " + "wrong name {}".format(name)) def _update_representations(self, upd_representations): """Frames dont have sense for textures, add collected udims instead.""" diff --git a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py index 63dcb82e83..7fe43c4203 100644 --- a/openpype/tools/standalonepublish/widgets/widget_drop_frame.py +++ b/openpype/tools/standalonepublish/widgets/widget_drop_frame.py @@ -211,7 +211,8 @@ class DropDataFrame(QtWidgets.QFrame): folder_path = os.path.dirname(collection.head) if file_base[-1] in ['.', '_']: file_base = file_base[:-1] - file_ext = collection.tail + file_ext = os.path.splitext( + collection.format('{head}{padding}{tail}'))[1] repr_name = file_ext.replace('.', '') range = collection.format('{ranges}') From b6e28c19b4475e7b0c0f03954a8cf9cf4f80730c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 20 Jul 2021 13:54:25 +0200 Subject: [PATCH 175/333] Nuke: fixing loading and updating effects --- openpype/hosts/nuke/plugins/load/load_effects.py | 2 +- .../hosts/nuke/plugins/load/load_effects_ip.py | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_effects.py b/openpype/hosts/nuke/plugins/load/load_effects.py index 6306767f37..8ba1b6b7c1 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects.py +++ b/openpype/hosts/nuke/plugins/load/load_effects.py @@ -214,7 +214,7 @@ class LoadEffects(api.Loader): self.log.warning(e) continue - if isinstance(v, list) and len(v) > 3: + if isinstance(v, list) and len(v) > 4: node[k].setAnimated() for i, value in enumerate(v): if isinstance(value, list): diff --git a/openpype/hosts/nuke/plugins/load/load_effects_ip.py b/openpype/hosts/nuke/plugins/load/load_effects_ip.py index 6c71f2ae16..d0cab26842 100644 --- a/openpype/hosts/nuke/plugins/load/load_effects_ip.py +++ b/openpype/hosts/nuke/plugins/load/load_effects_ip.py @@ -217,7 +217,7 @@ class LoadEffectsInputProcess(api.Loader): self.log.warning(e) continue - if isinstance(v, list) and len(v) > 3: + if isinstance(v, list) and len(v) > 4: node[k].setAnimated() for i, value in enumerate(v): if isinstance(value, list): @@ -239,10 +239,10 @@ class LoadEffectsInputProcess(api.Loader): output = nuke.createNode("Output") output.setInput(0, pre_node) - # try to place it under Viewer1 - if not self.connect_active_viewer(GN): - nuke.delete(GN) - return + # # try to place it under Viewer1 + # if not self.connect_active_viewer(GN): + # nuke.delete(GN) + # return # get all versions in list versions = io.find({ @@ -298,7 +298,11 @@ class LoadEffectsInputProcess(api.Loader): viewer["input_process_node"].setValue(group_node_name) # put backdrop under - lib.create_backdrop(label="Input Process", layer=2, nodes=[viewer, group_node], color="0x7c7faaff") + lib.create_backdrop( + label="Input Process", + layer=2, + nodes=[viewer, group_node], + color="0x7c7faaff") return True From 246a3cff52f0f4d6bd69aff4dafc44260f01d7de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Jul 2021 18:05:17 +0200 Subject: [PATCH 176/333] create the project in avalon if does not exist yet --- .../action_prepare_project.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index 5298c06371..e9b0b2a58a 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -1,6 +1,8 @@ import json +from avalon.api import AvalonMongoDB from openpype.api import ProjectSettings +from openpype.lib import create_project from openpype.modules.ftrack.lib import ( BaseAction, @@ -48,13 +50,22 @@ class PrepareProjectLocal(BaseAction): project_entity = entities[0] project_name = project_entity["full_name"] - try: - project_settings = ProjectSettings(project_name) - except ValueError: - return { - "message": "Project is not synchronized yet", - "success": False - } + # Try to find project document + dbcon = AvalonMongoDB() + dbcon.install() + dbcon.Session["AVALON_PROJECT"] = project_name + project_doc = dbcon.find_one({ + "type": "project" + }) + # Create project if is not available + # - creation is required to be able set project anatomy and attributes + if not project_doc: + project_code = project_entity["name"] + create_project(project_name, project_code, dbcon=dbcon) + + dbcon.uninstall() + + project_settings = ProjectSettings(project_name) project_anatom_settings = project_settings["project_anatomy"] root_items = self.prepare_root_items(project_anatom_settings) From 0e38083a014446acc56e0043d67e0f25914d96e9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Jul 2021 18:05:27 +0200 Subject: [PATCH 177/333] add basic order of attributes --- .../action_prepare_project.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index e9b0b2a58a..4bcd058ca8 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -26,6 +26,21 @@ class PrepareProjectLocal(BaseAction): # Key to store info about trigerring create folder structure item_splitter = {"type": "label", "value": "---"} + _keys_order = ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "clipIn", + "clipOut", + "resolutionHeight", + "resolutionWidth", + "pixelAspect", + "applications", + "tools_env", + "library_project", + ) def discover(self, session, entities, event): """Show only on project.""" @@ -211,7 +226,18 @@ class PrepareProjectLocal(BaseAction): str([key for key in attributes_to_set]) )) - for key, in_data in attributes_to_set.items(): + attribute_keys = set(attributes_to_set.keys()) + keys_order = [] + for key in self._keys_order: + if key in attribute_keys: + keys_order.append(key) + + attribute_keys = attribute_keys - set(keys_order) + for key in sorted(attribute_keys): + keys_order.append(key) + + for key in keys_order: + in_data = attributes_to_set[key] attr = in_data["object"] # initial item definition From 1f4c644fc0aab6d64d34762c799bfad34a13b3b4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 20 Jul 2021 18:28:56 +0200 Subject: [PATCH 178/333] create project when values are confirmed, not before --- .../action_prepare_project.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index 4bcd058ca8..5c40ec0d30 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -65,21 +65,6 @@ class PrepareProjectLocal(BaseAction): project_entity = entities[0] project_name = project_entity["full_name"] - # Try to find project document - dbcon = AvalonMongoDB() - dbcon.install() - dbcon.Session["AVALON_PROJECT"] = project_name - project_doc = dbcon.find_one({ - "type": "project" - }) - # Create project if is not available - # - creation is required to be able set project anatomy and attributes - if not project_doc: - project_code = project_entity["name"] - create_project(project_name, project_code, dbcon=dbcon) - - dbcon.uninstall() - project_settings = ProjectSettings(project_name) project_anatom_settings = project_settings["project_anatomy"] @@ -375,7 +360,27 @@ class PrepareProjectLocal(BaseAction): self.log.debug("Setting Custom Attribute values") - project_name = entities[0]["full_name"] + project_entity = entities[0] + project_name = project_entity["full_name"] + + # Try to find project document + dbcon = AvalonMongoDB() + dbcon.install() + dbcon.Session["AVALON_PROJECT"] = project_name + project_doc = dbcon.find_one({ + "type": "project" + }) + # Create project if is not available + # - creation is required to be able set project anatomy and attributes + if not project_doc: + project_code = project_entity["name"] + self.log.info("Creating project \"{} [{}]\"".format( + project_name, project_code + )) + create_project(project_name, project_code, dbcon=dbcon) + + dbcon.uninstall() + project_settings = ProjectSettings(project_name) project_anatomy_settings = project_settings["project_anatomy"] project_anatomy_settings["roots"] = root_data From 33d26d85bdcd1961c9dadad5c42299209224c025 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 21 Jul 2021 03:42:15 +0000 Subject: [PATCH 179/333] [Automated] Bump version --- CHANGELOG.md | 14 ++++++++------ openpype/version.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 467ed7c0a4..0ecd583191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,34 @@ # Changelog -## [3.3.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) **🚀 Enhancements** - nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829) +- Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823) - Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) +- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) - Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) **🐛 Bug fixes** +- nuke: write render node skipped with crop [\#1836](https://github.com/pypeclub/OpenPype/pull/1836) - Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) - Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) +**Merged pull requests:** + +- PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811) + ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0) **🚀 Enhancements** -- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) - Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799) - Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) - Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) @@ -110,20 +116,16 @@ - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) - OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) - Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) -- \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683) -- Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682) **🐛 Bug fixes** - Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) -- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) **Merged pull requests:** - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) -- Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) # Changelog diff --git a/openpype/version.py b/openpype/version.py index 00df9438eb..bbf93baec0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.2" +__version__ = "3.3.0-nightly.3" From 228829e81a4e27021a4e344345b223821bd597e4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 21 Jul 2021 17:20:36 +0200 Subject: [PATCH 180/333] various fixes --- openpype/hosts/maya/api/commands.py | 32 +++++++++++++++---- openpype/hosts/maya/api/menu.py | 20 ++++++++---- .../maya/api/shader_definition_editor.py | 29 +++++++++-------- .../plugins/publish/validate_model_name.py | 10 ++++-- .../python/common/scriptsmenu/action.py | 3 +- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py index 645e5840fd..4d37288b4e 100644 --- a/openpype/hosts/maya/api/commands.py +++ b/openpype/hosts/maya/api/commands.py @@ -3,6 +3,28 @@ import sys +class ToolWindows: + + _windows = {} + + @classmethod + def get_window(cls, tool, window=None): + # type: (str, QtWidgets.QWidget) -> QtWidgets.QWidget + try: + return cls._windows[tool] + except KeyError: + if window: + cls.set_window(tool, window) + return window + else: + return None + + @classmethod + def set_window(cls, tool, window): + # type: (str, QtWidget.QWidget) -> None + cls._windows[tool] = window + + def edit_shader_definitions(): from avalon.tools import lib from Qt import QtWidgets @@ -10,15 +32,13 @@ def edit_shader_definitions(): ShaderDefinitionsEditor ) - module = sys.modules[__name__] - module.window = None - top_level_widgets = QtWidgets.QApplication.topLevelWidgets() main_window = next(widget for widget in top_level_widgets if widget.objectName() == "MayaWindow") with lib.application(): - window = ShaderDefinitionsEditor(parent=main_window) + window = ToolWindows.get_window("shader_definition_editor") + if not window: + window = ShaderDefinitionsEditor(parent=main_window) + ToolWindows.set_window("shader_definition_editor", window) window.show() - - module.window = window diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index a8812210a5..0dced48868 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -9,8 +9,6 @@ import maya.cmds as cmds from openpype.settings import get_project_settings self = sys.modules[__name__] -project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) -self._menu = project_settings["maya"]["scriptsmenu"]["name"] log = logging.getLogger(__name__) @@ -19,8 +17,11 @@ log = logging.getLogger(__name__) def _get_menu(menu_name=None): """Return the menu instance if it currently exists in Maya""" + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + _menu = project_settings["maya"]["scriptsmenu"]["name"] + if menu_name is None: - menu_name = self._menu + menu_name = _menu widgets = dict(( w.objectName(), w) for w in QtWidgets.QApplication.allWidgets()) menu = widgets.get(menu_name) @@ -74,12 +75,18 @@ def deferred(): return # load configuration of custom menu + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) config = project_settings["maya"]["scriptsmenu"]["definition"] + _menu = project_settings["maya"]["scriptsmenu"]["name"] + + if not config: + log.warning("Skipping studio menu, no definition found.") + return # run the launcher for Maya menu studio_menu = launchformaya.main( - title=self._menu.title(), - objectName=self._menu + title=_menu.title(), + objectName=_menu.title().lower().replace(" ", "_") ) # apply configuration @@ -109,9 +116,8 @@ def install(): def popup(): - """Pop-up the existing menu near the mouse cursor""" + """Pop-up the existing menu near the mouse cursor.""" menu = _get_menu() - cursor = QtGui.QCursor() point = cursor.pos() menu.exec_(point) diff --git a/openpype/hosts/maya/api/shader_definition_editor.py b/openpype/hosts/maya/api/shader_definition_editor.py index 5585c9ea8e..73cc6246ab 100644 --- a/openpype/hosts/maya/api/shader_definition_editor.py +++ b/openpype/hosts/maya/api/shader_definition_editor.py @@ -11,11 +11,14 @@ from openpype import resources import gridfs +DEFINITION_FILENAME = "{}/maya/shader_definition.txt".format( + os.getenv("AVALON_PROJECT")) + + class ShaderDefinitionsEditor(QtWidgets.QWidget): """Widget serving as simple editor for shader name definitions.""" # name of the file used to store definitions - DEFINITION_FILENAME = "maya/shader_definition.txt" def __init__(self, parent=None): super(ShaderDefinitionsEditor, self).__init__(parent) @@ -78,7 +81,7 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): content = "" if not file: file = self._gridfs.find_one( - {"filename": self.DEFINITION_FILENAME}) + {"filename": DEFINITION_FILENAME}) if not file: print(">>> [SNDE]: nothing in database yet") return content @@ -102,7 +105,7 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): editor is running. """ file = self._gridfs.find_one( - {"filename": self.DEFINITION_FILENAME}) + {"filename": DEFINITION_FILENAME}) if file: content_check = self._read_definition_file(file) if content == content_check: @@ -116,7 +119,7 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): self._gridfs.delete(file._id) file = self._gridfs.new_file( - filename=self.DEFINITION_FILENAME, + filename=DEFINITION_FILENAME, content_type='text/plain', encoding='utf-8') file.write(content) @@ -134,7 +137,11 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): self._editor.setStyleSheet("border: none;") def _close(self): - self.close() + self.hide() + + def closeEvent(self, event): + event.ignore() + self.hide() def _reload(self): print(">>> [SNDE]: reloading") @@ -156,16 +163,10 @@ class ShaderDefinitionsEditor(QtWidgets.QWidget): self, "Warning", ("Content you are editing was changed meanwhile in database.\n" - "Do you want to overwrite it?"), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + "Please, reload and solve the conflict."), + QtWidgets.QMessageBox.OK) - if reply == QtWidgets.QMessageBox.Yes: - self._write_definition_file( - content=self._editor.toPlainText(), - force=True - ) - - elif reply == QtWidgets.QMessageBox.No: + if reply == QtWidgets.QMessageBox.OK: # do nothing pass diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index 84242cda23..42471b7877 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -4,6 +4,8 @@ from maya import cmds import pyblish.api import openpype.api import openpype.hosts.maya.api.action +from openpype.hosts.maya.api.shader_definition_editor import ( + DEFINITION_FILENAME) from openpype.lib.mongo import OpenPypeMongoConnection import gridfs import re @@ -25,12 +27,13 @@ class ValidateModelName(pyblish.api.InstancePlugin): label = "Model Name" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] material_file = None - database_file = "maya/shader_definition.txt" + database_file = DEFINITION_FILENAME @classmethod def get_invalid(cls, instance): """Get invalid nodes.""" - use_db = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["database"] # noqa: E501 + # use_db = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["database"] # noqa: E501 + use_db = cls.database def is_group(group_name): """Find out if supplied transform is group or not.""" @@ -84,7 +87,8 @@ class ValidateModelName(pyblish.api.InstancePlugin): shaders = map(lambda s: s.rstrip(), shaders) # compile regex for testing names - regex = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["regex"] # noqa: E501 + # regex = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["regex"] # noqa: E501 + regex = cls.regex r = re.compile(regex) for obj in filtered: diff --git a/openpype/vendor/python/common/scriptsmenu/action.py b/openpype/vendor/python/common/scriptsmenu/action.py index 5e68628406..dc4d775f6a 100644 --- a/openpype/vendor/python/common/scriptsmenu/action.py +++ b/openpype/vendor/python/common/scriptsmenu/action.py @@ -119,8 +119,7 @@ module.{module_name}()""" """ # get the current application and its linked keyboard modifiers - app = QtWidgets.QApplication.instance() - modifiers = app.keyboardModifiers() + modifiers = QtWidgets.QApplication.keyboardModifiers() # If the menu has a callback registered for the current modifier # we run the callback instead of the action itself. From 941a4d51ab9cb626b8d7df4dd9c7acbaf3a1a272 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 21 Jul 2021 17:26:44 +0200 Subject: [PATCH 181/333] =?UTF-8?q?=F0=9F=90=A9=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/api/commands.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/commands.py b/openpype/hosts/maya/api/commands.py index 4d37288b4e..d4c2b6a225 100644 --- a/openpype/hosts/maya/api/commands.py +++ b/openpype/hosts/maya/api/commands.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """OpenPype script commands to be used directly in Maya.""" -import sys class ToolWindows: @@ -8,20 +7,30 @@ class ToolWindows: _windows = {} @classmethod - def get_window(cls, tool, window=None): - # type: (str, QtWidgets.QWidget) -> QtWidgets.QWidget + def get_window(cls, tool): + """Get widget for specific tool. + + Args: + tool (str): Name of the tool. + + Returns: + Stored widget. + + """ try: return cls._windows[tool] except KeyError: - if window: - cls.set_window(tool, window) - return window - else: - return None + return None @classmethod def set_window(cls, tool, window): - # type: (str, QtWidget.QWidget) -> None + """Set widget for the tool. + + Args: + tool (str): Name of the tool. + window (QtWidgets.QWidget): Widget + + """ cls._windows[tool] = window From 558e71e8d53298d44a0aa1f47269c2373ac4e6ac Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 21 Jul 2021 17:27:50 +0200 Subject: [PATCH 182/333] minor cleanup --- openpype/hosts/maya/plugins/publish/validate_model_name.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index 42471b7877..64f06fb1fb 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -32,7 +32,6 @@ class ValidateModelName(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): """Get invalid nodes.""" - # use_db = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["database"] # noqa: E501 use_db = cls.database def is_group(group_name): @@ -87,7 +86,6 @@ class ValidateModelName(pyblish.api.InstancePlugin): shaders = map(lambda s: s.rstrip(), shaders) # compile regex for testing names - # regex = instance.context.data["project_settings"]["maya"]["publish"]["ValidateModelName"]["regex"] # noqa: E501 regex = cls.regex r = re.compile(regex) From 8b3aeddaa7cee1383fad72192c3f3308fa97010a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:33:00 +0200 Subject: [PATCH 183/333] added usefull methods to add traceback to job in ftrack base event handler --- .../modules/ftrack/lib/ftrack_base_handler.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index 817841df4a..ba8b065d34 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -1,4 +1,10 @@ +import os +import sys +import tempfile +import json import functools +import datetime +import traceback import time from openpype.api import Logger from openpype.settings import get_project_settings @@ -583,3 +589,105 @@ class BaseHandler(object): return "/".join( [ent["name"] for ent in entity["link"]] ) + + @classmethod + def add_traceback_to_job( + cls, job, session, exc_info, + description=None, + component_name=None, + job_status=None + ): + """Add traceback file to a job. + + Args: + job (JobEntity): Entity of job where file should be able to + download (Created or queried with passed session). + session (Session): Ftrack session which was used to query/create + entered job. + exc_info (tuple): Exception info (e.g. from `sys.exc_info()`). + description (str): Change job description to describe what + happened. Job description won't change if not passed. + component_name (str): Name of component and default name of + downloaded file. Class name and current date time are used if + not specified. + job_status (str): Status of job which will be set. By default is + set to 'failed'. + """ + if description: + job_data = { + "description": description + } + job["data"] = json.dumps(job_data) + + if not job_status: + job_status = "failed" + + job["status"] = job_status + + # Create temp file where traceback will be stored + temp_obj = tempfile.NamedTemporaryFile( + mode="w", prefix="openpype_ftrack_", suffix=".txt", delete=False + ) + temp_obj.close() + temp_filepath = temp_obj.name + + # Store traceback to file + result = traceback.format_exception(*exc_info) + with open(temp_filepath, "w") as temp_file: + temp_file.write("".join(result)) + + # Upload file with traceback to ftrack server and add it to job + if not component_name: + component_name = "{}_{}".format( + cls.__name__, + datetime.datetime.now().strftime("%y-%m-%d-%H%M") + ) + cls.add_component_to_job( + job, session, temp_filepath, component_name + ) + # Delete temp file + os.remove(temp_filepath) + + @staticmethod + def add_file_component_to_job(job, session, filepath, basename=None): + """Add filepath as downloadable component to job. + + Args: + job (JobEntity): Entity of job where file should be able to + download (Created or queried with passed session). + session (Session): Ftrack session which was used to query/create + entered job. + filepath (str): Path to file which should be added to job. + basename (str): Defines name of file which will be downloaded on + user's side. Must be without extension otherwise extension will + be duplicated in downloaded name. Basename from entered path + used when not entered. + """ + # Make sure session's locations are configured + # - they can be deconfigured e.g. using `rollback` method + session._configure_locations() + + # Query `ftrack.server` location where component will be stored + location = session.query( + "Location where name is \"ftrack.server\"" + ).one() + + # Use filename as basename if not entered (must be without extension) + if basename is None: + basename = os.path.splitext( + os.path.basename(filepath) + )[0] + + component = session.create_component( + filepath, + data={"name": basename}, + location=location + ) + session.create( + "JobComponent", + { + "component_id": component["id"], + "job_id": job["id"] + } + ) + session.commit() From 3f1305b88f88cdf88db068009b5f9e50bf19122c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:35:24 +0200 Subject: [PATCH 184/333] use add_traceback_to_job from base event handler to store traceback to a job --- .../action_push_frame_values_to_task.py | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py index 214f1ecf18..255ec252c2 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py @@ -1,3 +1,4 @@ +import sys import json import collections import ftrack_api @@ -90,27 +91,28 @@ class PushHierValuesToNonHier(ServerAction): try: result = self.propagate_values(session, event, entities) - job["status"] = "done" - session.commit() - - return result - - except Exception: - session.rollback() - job["status"] = "failed" - session.commit() + except Exception as exc: msg = "Pushing Custom attribute values to task Failed" + self.log.warning(msg, exc_info=True) + + session.rollback() + + description = "{} (Download traceback)".format(msg) + self.add_traceback_to_job( + job, session, sys.exc_info(), event, description + ) + return { "success": False, - "message": msg + "message": "Error: {}".format(str(exc)) } - finally: - if job["status"] == "running": - job["status"] = "failed" - session.commit() + job["status"] = "done" + session.commit() + + return result def attrs_configurations(self, session, object_ids, interest_attributes): attrs = session.query(self.cust_attrs_query.format( From d5c4009935f7021568f58c0f2ed1290184469a5f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:37:55 +0200 Subject: [PATCH 185/333] removed unused import --- openpype/modules/ftrack/lib/ftrack_base_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index ba8b065d34..c56412421f 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -1,5 +1,4 @@ import os -import sys import tempfile import json import functools From d96998626a70c300d20657288893ca86e358dc06 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:44:02 +0200 Subject: [PATCH 186/333] fix method name --- openpype/modules/ftrack/lib/ftrack_base_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index c56412421f..011ce8db9d 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -641,7 +641,7 @@ class BaseHandler(object): cls.__name__, datetime.datetime.now().strftime("%y-%m-%d-%H%M") ) - cls.add_component_to_job( + cls.add_file_component_to_job( job, session, temp_filepath, component_name ) # Delete temp file From c7dc3ac1015d841ea328e390b60fc7e3dfb0e827 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:44:21 +0200 Subject: [PATCH 187/333] fix args --- .../event_handlers_server/action_push_frame_values_to_task.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py index 255ec252c2..b38e18d089 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py @@ -101,7 +101,7 @@ class PushHierValuesToNonHier(ServerAction): description = "{} (Download traceback)".format(msg) self.add_traceback_to_job( - job, session, sys.exc_info(), event, description + job, session, sys.exc_info(), description ) return { From d2fa34b52b442f0b4f81754d8ded322fb8f6c6b0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:52:01 +0200 Subject: [PATCH 188/333] store scene frame start to context --- openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index d8bb03f541..79cc01740a 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -155,6 +155,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "sceneMarkInState": mark_in_state == "set", "sceneMarkOut": int(mark_out_frame), "sceneMarkOutState": mark_out_state == "set", + "sceneStartFrame": int(lib.execute_george("tv_startframe")), "sceneBgColor": self._get_bg_color() } self.log.debug( From bdd065a840418410132c8f12a71a17466946260a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:53:14 +0200 Subject: [PATCH 189/333] use scene start frame as an offset --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 536df2adb0..472d57db36 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -49,6 +49,14 @@ class ExtractSequence(pyblish.api.Extractor): family_lowered = instance.data["family"].lower() mark_in = instance.context.data["sceneMarkIn"] mark_out = instance.context.data["sceneMarkOut"] + + # Scene start frame offsets the output files, so we need to offset the + # marks. + scene_start_frame = instance.context.data["sceneStartFrame"] + difference = scene_start_frame - mark_in + mark_in += difference + mark_out += difference + # Frame start/end may be stored as float frame_start = int(instance.data["frameStart"]) frame_end = int(instance.data["frameEnd"]) From f079508c20fb86b7acba414b2ffc038c61f759bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:53:22 +0200 Subject: [PATCH 190/333] fix variable name --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 472d57db36..1df7512588 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -106,7 +106,7 @@ class ExtractSequence(pyblish.api.Extractor): self.log.warning(( "Lowering representation range to {} frames." " Changed frame end {} -> {}" - ).format(output_range + 1, mark_out, new_mark_out)) + ).format(output_range + 1, mark_out, new_output_frame_end)) output_frame_end = new_output_frame_end # ------------------------------------------------------------------- From 5067d18cdafe383b6063e346ecaacfa603ba26b0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 21 Jul 2021 17:55:11 +0200 Subject: [PATCH 191/333] =?UTF-8?q?add=20=F0=9F=A7=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/docs/admin_hosts_maya.md | 25 +++++++++++++++++- .../maya-admin_model_name_validator.png | Bin 0 -> 19794 bytes .../docs/assets/maya-admin_scriptsmenu.png | Bin 0 -> 16565 bytes 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 website/docs/assets/maya-admin_model_name_validator.png create mode 100644 website/docs/assets/maya-admin_scriptsmenu.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 83c4121be9..81aa64f9d6 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -49,4 +49,27 @@ Arnolds Camera (AA) samples to 6. Note that `aiOptions` is not the name of node but rather its type. For renderers there is usually just one instance of this node type but if that is not so, validator will go through all its instances and check the value there. Node type for **VRay** settings is `VRaySettingsNode`, for **Renderman** -it is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`. \ No newline at end of file +it is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`. + +#### Model Name Validator (`ValidateRenderSettings`) +This validator can enforce specific names for model members. It will check them against **Validation Regex**. +There is special group in that regex - **shader**. If present, it will take that part of the name as shader name +and it will compare it with list of shaders defined either in file name specified in **Material File** or from +database file that is per project and can be directly edited from Maya's *OpenPype Tools > Edit Shader name definitions* when +**Use database shader name definitions** is on. This list defines simply as one shader name per line. + +![Settings example](assets/maya-admin_model_name_validator.png) + +For example - you are using default regex `(.*)_(\d)*_(?P.*)_(GEO)` and you have two shaders defined +in either file or database `foo` and `bar`. + +Object named `SomeCube_0001_foo_GEO` will pass but `SomeCube_GEO` will not and `SomeCube_001_xxx_GEO` will not too. + +### Custom Menu +You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**. +![Custom menu definition](assets/maya-admin_scriptsmenu.png) + +:::note Work in progress +This is still work in progress. Menu definition will be handled more friendly with widgets and not +raw json. +::: \ No newline at end of file diff --git a/website/docs/assets/maya-admin_model_name_validator.png b/website/docs/assets/maya-admin_model_name_validator.png new file mode 100644 index 0000000000000000000000000000000000000000..39ec2b2d211a27ac7962dadaaae4c2af4241f797 GIT binary patch literal 19794 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 Vy(H!b1HAwM002ovPDHLkV1k)@_e%f( literal 0 HcmV?d00001 diff --git a/website/docs/assets/maya-admin_scriptsmenu.png b/website/docs/assets/maya-admin_scriptsmenu.png new file mode 100644 index 0000000000000000000000000000000000000000..ecfe7e42a74db2fb543d813ee83c842e924d3d87 GIT binary patch literal 16565 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 literal 0 HcmV?d00001 From 2b38639f9ef427b56fe9cc26f29634fd50d68fa7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:58:15 +0200 Subject: [PATCH 192/333] added validator for checking start frame --- .../plugins/publish/validate_start_frame.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py new file mode 100644 index 0000000000..d769d47736 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py @@ -0,0 +1,27 @@ +import pyblish.api +from avalon.tvpaint import lib + + +class RepairStartFrame(pyblish.api.Action): + """Repair start frame.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + lib.execute_george("tv_startframe 0") + + +class ValidateStartFrame(pyblish.api.ContextPlugin): + """Validate start frame being at frame 0.""" + + label = "Validate Start Frame" + order = pyblish.api.ValidatorOrder + hosts = ["tvpaint"] + actions = [RepairStartFrame] + optional = True + + def process(self, context): + start_frame = lib.execute_george("tv_startframe") + assert int(start_frame) == 0, "Start frame has to be frame 0." From 4e9ee047ae573c7cfe7c97bc6e43e6e7bd51630d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 21 Jul 2021 17:58:22 +0200 Subject: [PATCH 193/333] added settings for new validator which is turned off by default --- .../settings/defaults/project_settings/tvpaint.json | 5 +++++ .../projects_schema/schema_project_tvpaint.json | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 763802a73f..47f486aa98 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -18,6 +18,11 @@ "optional": true, "active": true }, + "ValidateStartFrame": { + "enabled": false, + "optional": true, + "active": true + }, "ValidateAssetName": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 67aa4b0a06..368141813f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -52,6 +52,17 @@ } ] }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateStartFrame", + "label": "Validate Scene Start Frame", + "docstring": "Validate first frame of scene is set to '0'." + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", From 303f2d08cf075c13c1e172d7ef87370194910500 Mon Sep 17 00:00:00 2001 From: jezscha Date: Mon, 24 May 2021 14:39:27 +0000 Subject: [PATCH 194/333] Create draft PR for #1002 --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index d8be0bdb37..cfd4191e36 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit d8be0bdb37961e32243f1de0eb9696e86acf7443 +Subproject commit cfd4191e364b47de7364096f45d9d9d9a901692a From f804d4bd40cc5afc260c3a757046a3c8e385d248 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 22 Jul 2021 16:07:15 +0200 Subject: [PATCH 195/333] pass right type to get_hierarchical_attributes_values --- .../ftrack/event_handlers_server/event_sync_to_avalon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py index e60045bd50..1dd056adee 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py @@ -1259,7 +1259,7 @@ class SyncToAvalonEvent(BaseEvent): self.process_session, entity, hier_attrs, - self.cust_attr_types_by_id + self.cust_attr_types_by_id.values() ) for key, val in hier_values.items(): output[key] = val From e1e3dd4dd5cd95f6553190b3d690655d8e228d63 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 22 Jul 2021 21:41:41 +0200 Subject: [PATCH 196/333] Textures - fixed defaults Broken file name shouldnt fail in collect --- .../plugins/publish/collect_texture.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index 439168ea10..d70a0a75b8 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -41,13 +41,13 @@ class CollectTextures(pyblish.api.ContextPlugin): color_space = ["linsRGB", "raw", "acesg"] - #currently implemented placeholders ["color_space"] + # currently implemented placeholders ["color_space"] # describing patterns in file names splitted by regex groups input_naming_patterns = { # workfile: corridorMain_v001.mra > # texture: corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr "workfile": r'^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+', - "textures": r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', + "textures": r'^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+', # noqa } # matching regex group position to 'input_naming_patterns' input_naming_groups = { @@ -168,10 +168,10 @@ class CollectTextures(pyblish.api.ContextPlugin): ) formatting_data = { - "color_space": c_space, - "channel": channel, - "shader": shader, - "subset": parsed_subset + "color_space": c_space or '', # None throws exception + "channel": channel or '', + "shader": shader or '', + "subset": parsed_subset or '' } fill_pairs = prepare_template_data(formatting_data) @@ -195,9 +195,9 @@ class CollectTextures(pyblish.api.ContextPlugin): representations[subset].append(repre) ver_data = { - "color_space": c_space, - "channel_name": channel, - "shader_name": shader + "color_space": c_space or '', + "channel_name": channel or '', + "shader_name": shader or '' } version_data[subset] = ver_data @@ -251,7 +251,7 @@ class CollectTextures(pyblish.api.ContextPlugin): "label": subset, "name": subset, "family": family, - "version": int(version or main_version), + "version": int(version or main_version or 1), "asset_build": asset_build # remove in validator } ) From f6bfce0ae0a412ba19129c9562c62770b3bd5b76 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Jul 2021 15:24:12 +0200 Subject: [PATCH 197/333] nuke: recreating creator node function --- openpype/hosts/nuke/api/lib.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index eefbcc5d20..5f898a9a67 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1739,3 +1739,68 @@ def process_workfile_builder(): log.info("Opening last workfile...") # open workfile open_file(last_workfile_path) + + +def recreate_instance(origin_node, avalon_data=None): + """Recreate input instance to different data + + Args: + origin_node (nuke.Node): Nuke node to be recreating from + avalon_data (dict, optional): data to be used in new node avalon_data + + Returns: + nuke.Node: newly created node + """ + knobs_wl = ["render", "publish", "review", "ypos", + "use_limit", "first", "last"] + # get data from avalon knobs + data = anlib.get_avalon_knob_data( + origin_node) + + # add input data to avalon data + if avalon_data: + data.update(avalon_data) + + # capture all node knobs allowed in op_knobs + knobs_data = {k: origin_node[k].value() + for k in origin_node.knobs() + for key in knobs_wl + if key in k} + + # get node dependencies + inputs = origin_node.dependencies() + outputs = origin_node.dependent() + + # remove the node + nuke.delete(origin_node) + + # create new node + # get appropriate plugin class + creator_plugin = None + for Creator in api.discover(api.Creator): + if Creator.__name__ == data["creator"]: + creator_plugin = Creator + break + + # create write node with creator + new_node_name = data["subset"] + new_node = creator_plugin(new_node_name, data["asset"]).process() + + # white listed knobs to the new node + for _k, _v in knobs_data.items(): + try: + print(_k, _v) + new_node[_k].setValue(_v) + except Exception as e: + print(e) + + # connect to original inputs + for i, n in enumerate(inputs): + new_node.setInput(i, n) + + # connect to outputs + if len(outputs) > 0: + for dn in outputs: + dn.setInput(0, new_node) + + return new_node From c64852c214400de0840442f69d414e6c6e821d0b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Jul 2021 15:24:46 +0200 Subject: [PATCH 198/333] global: changing context validator to use recreator for nuke --- .../publish/validate_instance_in_context.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/validate_instance_in_context.py b/openpype/plugins/publish/validate_instance_in_context.py index 29f002f142..61b4d82027 100644 --- a/openpype/plugins/publish/validate_instance_in_context.py +++ b/openpype/plugins/publish/validate_instance_in_context.py @@ -92,15 +92,16 @@ class RepairSelectInvalidInstances(pyblish.api.Action): context_asset = context.data["assetEntity"]["name"] for instance in instances: - self.set_attribute(instance, context_asset) + if "nuke" in pyblish.api.registered_hosts(): + import openpype.hosts.nuke.api as nuke_api + origin_node = instance[0] + nuke_api.lib.recreate_instance( + origin_node, avalon_data={"asset": context_asset} + ) + else: + self.set_attribute(instance, context_asset) def set_attribute(self, instance, context_asset): - if "nuke" in pyblish.api.registered_hosts(): - import nuke - nuke.toNode( - instance.data.get("name") - )["avalon:asset"].setValue(context_asset) - if "maya" in pyblish.api.registered_hosts(): from maya import cmds cmds.setAttr( From 5293718d0ca15593fb1020c758fef5370ebeff72 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 23 Jul 2021 16:36:10 +0200 Subject: [PATCH 199/333] publisher: missing version in subset prop --- .../plugins/publish/collect_editorial_instances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index dbf2574a9d..60a8cf48fc 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -181,7 +181,8 @@ class CollectInstances(pyblish.api.InstancePlugin): } }) for subset, properities in self.subsets.items(): - if properities["version"] == 0: + version = properities.get("version") + if version and version == 0: properities.pop("version") # adding Review-able instance From d57bf4ec9e4276412b279163f007dca5ba0f1dff Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 24 Jul 2021 03:41:46 +0000 Subject: [PATCH 200/333] [Automated] Bump version --- CHANGELOG.md | 16 +++++++--------- openpype/version.py | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ecd583191..f75f68a5bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,30 @@ # Changelog -## [3.3.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) **🚀 Enhancements** +- Ftrack push attributes action adds traceback to job [\#1843](https://github.com/pypeclub/OpenPype/pull/1843) +- Prepare project action enhance [\#1838](https://github.com/pypeclub/OpenPype/pull/1838) - nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829) - Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823) - Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) -- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) - Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) **🐛 Bug fixes** +- Nuke: updating effects subset fail [\#1841](https://github.com/pypeclub/OpenPype/pull/1841) - nuke: write render node skipped with crop [\#1836](https://github.com/pypeclub/OpenPype/pull/1836) - Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) - Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) +- Failsafe for cross project containers. [\#1806](https://github.com/pypeclub/OpenPype/pull/1806) - Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) **Merged pull requests:** +- Add support for pyenv-win on windows [\#1822](https://github.com/pypeclub/OpenPype/pull/1822) - PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811) ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) @@ -29,6 +33,7 @@ **🚀 Enhancements** +- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) - Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799) - Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) - Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) @@ -114,18 +119,11 @@ - Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) -- OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) -- Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) **🐛 Bug fixes** - Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) -- Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) - -**Merged pull requests:** - -- update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) # Changelog diff --git a/openpype/version.py b/openpype/version.py index bbf93baec0..55f4c21997 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.3" +__version__ = "3.3.0-nightly.4" From 6dfac0797ba355bd5a010169e26ef591d16a3d29 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 09:53:27 +0200 Subject: [PATCH 201/333] added funtion to load openpype default settings value --- openpype/settings/lib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 4a3e66de33..dcbfbf7334 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -315,6 +315,11 @@ class DuplicatedEnvGroups(Exception): super(DuplicatedEnvGroups, self).__init__(msg) +def load_openpype_default_settings(): + """Load openpype default settings.""" + return load_jsons_from_dir(DEFAULTS_DIR) + + def reset_default_settings(): global _DEFAULT_SETTINGS _DEFAULT_SETTINGS = None @@ -322,7 +327,7 @@ def reset_default_settings(): def get_default_settings(): # TODO add cacher - return load_jsons_from_dir(DEFAULTS_DIR) + return load_openpype_default_settings() # global _DEFAULT_SETTINGS # if _DEFAULT_SETTINGS is None: # _DEFAULT_SETTINGS = load_jsons_from_dir(DEFAULTS_DIR) From 1a5266e91698d9f153e5a5a25a98f0e85140058e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 10:40:07 +0200 Subject: [PATCH 202/333] added function to load general environments --- openpype/settings/lib.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index dcbfbf7334..d917b18d61 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -873,6 +873,25 @@ def get_environments(): return find_environments(get_system_settings(False)) +def get_general_environments(): + """Get general environments. + + Function is implemented to be able load general environments without using + `get_default_settings`. + """ + # Use only openpype defaults. + # - prevent to use `get_system_settings` where `get_default_settings` + # is used + default_values = load_openpype_default_settings() + studio_overrides = get_studio_system_settings_overrides() + result = apply_overrides(default_values, studio_overrides) + environments = result["general"]["environment"] + + clear_metadata_from_settings(environments) + + return environments + + def clear_metadata_from_settings(values): """Remove all metadata keys from loaded settings.""" if isinstance(values, dict): From 22876bbbdee42d76976223658a04886b3a94f682 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 10:40:18 +0200 Subject: [PATCH 203/333] added few docstrings --- openpype/settings/lib.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index d917b18d61..5c2c0dcd94 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -321,11 +321,20 @@ def load_openpype_default_settings(): def reset_default_settings(): + """Reset cache of default settings. Can't be used now.""" global _DEFAULT_SETTINGS _DEFAULT_SETTINGS = None def get_default_settings(): + """Get default settings. + + Todo: + Cache loaded defaults. + + Returns: + dict: Loaded default settings. + """ # TODO add cacher return load_openpype_default_settings() # global _DEFAULT_SETTINGS From 00ea737307da1af989fb7770e8212142a9853f25 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 10:40:46 +0200 Subject: [PATCH 204/333] start.py can use `get_general_environments` if is available --- start.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/start.py b/start.py index 1b5c25ae3a..419a956835 100644 --- a/start.py +++ b/start.py @@ -208,14 +208,21 @@ def set_openpype_global_environments() -> None: """Set global OpenPype's environments.""" import acre - from openpype.settings import get_environments + try: + from openpype.settings import get_general_environments - all_env = get_environments() + general_env = get_general_environments() + + except Exception: + # Backwards compatibility for OpenPype versions where + # `get_general_environments` does not exists yet + from openpype.settings import get_environments + + all_env = get_environments() + general_env = all_env["global"] - # TODO Global environments will be stored in "general" settings so loading - # will be modified and can be done in igniter. env = acre.merge( - acre.parse(all_env["global"]), + acre.parse(general_env), dict(os.environ) ) os.environ.clear() From 295e400c81fca122e9dbda7b1b337697e4482b67 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 11:38:50 +0200 Subject: [PATCH 205/333] BaseAction has identifier id added to end of class identifier --- .../modules/ftrack/lib/ftrack_action_handler.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 2bff9d8cb3..6994ecc4dd 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -1,4 +1,5 @@ import os +from uuid import uuid4 from .ftrack_base_handler import BaseHandler @@ -29,6 +30,10 @@ class BaseAction(BaseHandler): icon = None type = 'Action' + # Modified identifier used for local actions + _identifier = None + _identifier_id = str(uuid4()) + settings_frack_subkey = "user_handlers" settings_enabled_key = "enabled" @@ -42,6 +47,14 @@ class BaseAction(BaseHandler): super().__init__(session) + def get_identifier(self): + """Modify identifier to trigger the action only on once machine.""" + if self._identifier is None: + self._identifier = "{}.{}".format( + self.identifier, self._identifier_id + ) + return self._identifier + def register(self): ''' Registers the action, subscribing the the discover and launch topics. @@ -60,7 +73,7 @@ class BaseAction(BaseHandler): ' and data.actionIdentifier={0}' ' and source.user.username={1}' ).format( - self.identifier, + self.get_identifier(), self.session.api_user ) self.session.event_hub.subscribe( @@ -86,7 +99,7 @@ class BaseAction(BaseHandler): 'label': self.label, 'variant': self.variant, 'description': self.description, - 'actionIdentifier': self.identifier, + 'actionIdentifier': self.get_identifier(), 'icon': self.icon, }] } From 607cc6e2ea75fb3a711afc5f3d3665bcb9fb8483 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 11:39:05 +0200 Subject: [PATCH 206/333] override `get_identifier` for server action --- openpype/modules/ftrack/lib/ftrack_action_handler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 6994ecc4dd..9d005eb876 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -331,6 +331,12 @@ class ServerAction(BaseAction): settings_frack_subkey = "events" + def get_identifier(self): + """Override default implementation to not add identifier id.""" + if self._identifier is None: + self._identifier = self.identifier + return self._identifier + def register(self): """Register subcription to Ftrack event hub.""" self.session.event_hub.subscribe( @@ -341,5 +347,5 @@ class ServerAction(BaseAction): launch_subscription = ( "topic=ftrack.action.launch and data.actionIdentifier={0}" - ).format(self.identifier) + ).format(self.get_identifier()) self.session.event_hub.subscribe(launch_subscription, self._launch) From e1b10317da98691abc8737e62db9c1baf5d82b84 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 12:00:47 +0200 Subject: [PATCH 207/333] removed all added stuff --- .../ftrack/lib/ftrack_action_handler.py | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 9d005eb876..06152c19f7 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -30,10 +30,6 @@ class BaseAction(BaseHandler): icon = None type = 'Action' - # Modified identifier used for local actions - _identifier = None - _identifier_id = str(uuid4()) - settings_frack_subkey = "user_handlers" settings_enabled_key = "enabled" @@ -47,14 +43,6 @@ class BaseAction(BaseHandler): super().__init__(session) - def get_identifier(self): - """Modify identifier to trigger the action only on once machine.""" - if self._identifier is None: - self._identifier = "{}.{}".format( - self.identifier, self._identifier_id - ) - return self._identifier - def register(self): ''' Registers the action, subscribing the the discover and launch topics. @@ -73,7 +61,7 @@ class BaseAction(BaseHandler): ' and data.actionIdentifier={0}' ' and source.user.username={1}' ).format( - self.get_identifier(), + self.identifier, self.session.api_user ) self.session.event_hub.subscribe( @@ -99,7 +87,7 @@ class BaseAction(BaseHandler): 'label': self.label, 'variant': self.variant, 'description': self.description, - 'actionIdentifier': self.get_identifier(), + 'actionIdentifier': self.identifier, 'icon': self.icon, }] } @@ -331,12 +319,6 @@ class ServerAction(BaseAction): settings_frack_subkey = "events" - def get_identifier(self): - """Override default implementation to not add identifier id.""" - if self._identifier is None: - self._identifier = self.identifier - return self._identifier - def register(self): """Register subcription to Ftrack event hub.""" self.session.event_hub.subscribe( @@ -347,5 +329,5 @@ class ServerAction(BaseAction): launch_subscription = ( "topic=ftrack.action.launch and data.actionIdentifier={0}" - ).format(self.get_identifier()) + ).format(self.identifier) self.session.event_hub.subscribe(launch_subscription, self._launch) From 5c9a7d10486ae543688c4dcee06797dc8323e130 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 26 Jul 2021 12:21:14 +0200 Subject: [PATCH 208/333] added discover and launch identifier properties for actions --- .../action_applications.py | 2 +- .../ftrack/lib/ftrack_action_handler.py | 34 +++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_applications.py b/openpype/modules/ftrack/event_handlers_user/action_applications.py index 23c96e1b9f..58ea3c5671 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_applications.py +++ b/openpype/modules/ftrack/event_handlers_user/action_applications.py @@ -29,7 +29,7 @@ class AppplicationsAction(BaseAction): icon_url = os.environ.get("OPENPYPE_STATICS_SERVER") def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + super(AppplicationsAction, self).__init__(*args, **kwargs) self.application_manager = ApplicationManager() self.dbcon = AvalonMongoDB() diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 06152c19f7..878eac6627 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -30,6 +30,10 @@ class BaseAction(BaseHandler): icon = None type = 'Action' + _identifier_id = str(uuid4()) + _discover_identifier = None + _launch_identifier = None + settings_frack_subkey = "user_handlers" settings_enabled_key = "enabled" @@ -43,6 +47,22 @@ class BaseAction(BaseHandler): super().__init__(session) + @property + def discover_identifier(self): + if self._discover_identifier is None: + self._discover_identifier = "{}.{}".format( + self.identifier, self._identifier_id + ) + return self._discover_identifier + + @property + def launch_identifier(self): + if self._launch_identifier is None: + self._launch_identifier = "{}.{}".format( + self.identifier, self._identifier_id + ) + return self._launch_identifier + def register(self): ''' Registers the action, subscribing the the discover and launch topics. @@ -61,7 +81,7 @@ class BaseAction(BaseHandler): ' and data.actionIdentifier={0}' ' and source.user.username={1}' ).format( - self.identifier, + self.launch_identifier, self.session.api_user ) self.session.event_hub.subscribe( @@ -87,7 +107,7 @@ class BaseAction(BaseHandler): 'label': self.label, 'variant': self.variant, 'description': self.description, - 'actionIdentifier': self.identifier, + 'actionIdentifier': self.discover_identifier, 'icon': self.icon, }] } @@ -319,6 +339,14 @@ class ServerAction(BaseAction): settings_frack_subkey = "events" + @property + def discover_identifier(self): + return self.identifier + + @property + def launch_identifier(self): + return self.identifier + def register(self): """Register subcription to Ftrack event hub.""" self.session.event_hub.subscribe( @@ -329,5 +357,5 @@ class ServerAction(BaseAction): launch_subscription = ( "topic=ftrack.action.launch and data.actionIdentifier={0}" - ).format(self.identifier) + ).format(self.launch_identifier) self.session.event_hub.subscribe(launch_subscription, self._launch) From f3f2d96bd370eefb16e52270c41a93ae43f547a0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 26 Jul 2021 13:22:24 +0200 Subject: [PATCH 209/333] imageio: fix grouping --- .../projects_schema/schemas/schema_anatomy_imageio.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 2b2eab8868..3c589f9492 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -3,6 +3,7 @@ "key": "imageio", "label": "Color Management and Output Formats", "is_file": true, + "is_group": true, "children": [ { "key": "hiero", @@ -14,7 +15,6 @@ "type": "dict", "label": "Workfile", "collapsible": false, - "is_group": true, "children": [ { "type": "form", @@ -89,7 +89,6 @@ "type": "dict", "label": "Colorspace on Inputs by regex detection", "collapsible": true, - "is_group": true, "children": [ { "type": "list", @@ -124,7 +123,6 @@ "type": "dict", "label": "Viewer", "collapsible": false, - "is_group": true, "children": [ { "type": "text", @@ -138,7 +136,6 @@ "type": "dict", "label": "Workfile", "collapsible": false, - "is_group": true, "children": [ { "type": "form", @@ -236,7 +233,6 @@ "type": "dict", "label": "Nodes", "collapsible": true, - "is_group": true, "children": [ { "key": "requiredNodes", @@ -339,7 +335,6 @@ "type": "dict", "label": "Colorspace on Inputs by regex detection", "collapsible": true, - "is_group": true, "children": [ { "type": "list", From c250df6b57616d18c1ecb335b8145621eb5b33fd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 26 Jul 2021 13:47:22 +0200 Subject: [PATCH 210/333] Textures publishing - fix - missing field --- .../plugins/publish/extract_workfile_location.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py index f91851c201..18bf0394ae 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_workfile_location.py @@ -26,6 +26,7 @@ class ExtractWorkfileUrl(pyblish.api.ContextPlugin): template_data = instance.data.get("anatomyData") rep_name = instance.data.get("representations")[0].get("name") template_data["representation"] = rep_name + template_data["ext"] = rep_name anatomy_filled = anatomy.format(template_data) template_filled = anatomy_filled["publish"]["path"] filepath = os.path.normpath(template_filled) From abda7f9afa6092631a4162ecf00739a9da039fb4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 26 Jul 2021 13:57:23 +0200 Subject: [PATCH 211/333] Textures publishing - added additional example for textures --- .../settings_project_standalone.md | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/website/docs/project_settings/settings_project_standalone.md b/website/docs/project_settings/settings_project_standalone.md index 5180486d29..b359dc70d0 100644 --- a/website/docs/project_settings/settings_project_standalone.md +++ b/website/docs/project_settings/settings_project_standalone.md @@ -49,19 +49,36 @@ Provide regex matching pattern containing regex groups used to parse workfile na build name.) Example: -```^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+``` - parses `corridorMain_v001` into three groups: + +- pattern: ```^([^.]+)(_[^_.]*)?_v([0-9]{3,}).+``` +- with groups: ```["asset", "filler", "version"]``` + +parses `corridorMain_v001` into three groups: - asset build (`corridorMain`) - filler (in this case empty) - version (`001`) -In case of different naming pattern, additional groups could be added or removed. +Advanced example (for texture files): + +- pattern: ```^([^_.]+)_([^_.]+)_v([0-9]{3,})_([^_.]+)_({color_space})_(1[0-9]{3}).+``` +- with groups: ```["asset", "shader", "version", "channel", "color_space", "udim"]``` + +parses `corridorMain_aluminiumID_v001_baseColor_linsRGB_1001.exr`: +- asset build (`corridorMain`) +- shader (`aluminiumID`) +- version (`001`) +- channel (`baseColor`) +- color_space (`linsRGB`) +- udim (`1001`) + + +In case of different naming pattern, additional groups could be added or removed. Number of matching groups (`(...)`) must be same as number of items in `Group order for regex patterns` ##### Workfile group positions For each matching regex group set in previous paragraph, its ordinal position is required (in case of need for addition of new groups etc.) -Number of groups added here must match number of parsing groups from `Workfile naming pattern`. -Same configuration is available for texture files. +Number of groups added here must match number of parsing groups from `Workfile naming pattern`. ##### Output names From c31cdf94d18da2946d2c620460a7c5980527667b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 27 Jul 2021 10:30:26 +0200 Subject: [PATCH 212/333] initial support for configurable dirmap --- openpype/hosts/maya/api/__init__.py | 18 ++++++++++ .../defaults/project_settings/maya.json | 13 ++++++++ .../defaults/project_settings/unreal.json | 3 +- .../projects_schema/schema_project_maya.json | 33 +++++++++++++++++++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 4697d212de..027fa871e8 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -26,6 +26,24 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") def install(): + from openpype.settings import get_project_settings + + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} + if mapping.get("source-path") and project_settings["maya"]["maya-dirmap"]["enabled"] is True: + log.info("Processing directory mapping ...") + cmds.dirmap(en=True) + for k, sp in enumerate(mapping["source-path"]): + try: + print("{} -> {}".format(sp, mapping["destination-path"][k])) + cmds.dirmap(m=[sp, mapping["destination-path"][k]]) + cmds.dirmap(m=[mapping["destination-path"][k], sp]) + except IndexError: + # missing corresponding destination path + log.error(("invalid dirmap mapping, missing corresponding" + " destination directory.")) + break + pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) avalon.register_plugin_path(avalon.Creator, CREATE_PATH) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 284a1a0040..b92dc52b92 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -7,6 +7,19 @@ "workfile": "ma", "yetiRig": "ma" }, + "maya-dirmap": { + "enabled": true, + "paths": { + "source-path": [ + "foo1", + "foo2" + ], + "destination-path": [ + "bar1", + "bar2" + ] + } + }, "create": { "CreateLook": { "enabled": true, diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 46b9ca2a18..dad61cd1f0 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,6 +1,5 @@ { "project_setup": { - "dev_mode": true, - "install_unreal_python_engine": false + "dev_mode": true } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 0a59cab510..e70c0da708 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -14,6 +14,39 @@ "type": "text" } }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "maya-dirmap", + "label": "Maya Directory Mapping", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "dict", + "key": "paths", + "children": [ + { + "type": "list", + "object_type": "text", + "key": "source-path", + "label": "Source Path" + }, + { + "type": "list", + "object_type": "text", + "key": "destination-path", + "label": "Destination Path" + } + ] + } + ] + }, { "type": "schema", "name": "schema_maya_create" From 4b62088e1b2b273410b17a71db8a9450f9e6c892 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 10:43:48 +0200 Subject: [PATCH 213/333] added setting to check create project structure by default --- .../settings/defaults/project_settings/ftrack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 7cf5568662..dae5a591e9 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -136,7 +136,8 @@ "Pypeclub", "Administrator", "Project manager" - ] + ], + "create_project_structure_checked": false }, "clean_hierarchical_attr": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index a94ebc8888..1cc08b96f8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -441,6 +441,18 @@ "key": "role_list", "label": "Roles", "object_type": "text" + }, + { + "type": "separator" + }, + { + "type": "label", + "label": "Check \"Create project structure\" by default" + }, + { + "type": "boolean", + "key": "create_project_structure_checked", + "label": "Checked" } ] }, From aab871fea755064c78ab509024520b755a55884f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:09:24 +0200 Subject: [PATCH 214/333] use CUST_ATTR_AUTO_SYNC constance for custom attribute name --- .../ftrack/event_handlers_user/action_prepare_project.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index 5c40ec0d30..43b8f34dfd 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -90,14 +90,12 @@ class PrepareProjectLocal(BaseAction): items.extend(ca_items) - # This item will be last (before enumerators) - # - sets value of auto synchronization - auto_sync_name = "avalon_auto_sync" + # Set value of auto synchronization auto_sync_value = project_entity["custom_attributes"].get( CUST_ATTR_AUTO_SYNC, False ) auto_sync_item = { - "name": auto_sync_name, + "name": CUST_ATTR_AUTO_SYNC, "type": "boolean", "value": auto_sync_value, "label": "AutoSync to Avalon" From fb1a39bd83c56dc5ebd809ad8ac3a3e4a97275e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:09:43 +0200 Subject: [PATCH 215/333] commit custom attributes changes --- .../event_handlers_user/action_prepare_project.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index 43b8f34dfd..eddad851e3 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -393,10 +393,12 @@ class PrepareProjectLocal(BaseAction): project_settings.save() - entity = entities[0] - for key, value in custom_attribute_values.items(): - entity["custom_attributes"][key] = value - self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + # Change custom attributes on project + if custom_attribute_values: + for key, value in custom_attribute_values.items(): + project_entity["custom_attributes"][key] = value + self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + session.commit() return True From b180d7be2247f6e948cac9cedb81fec28f28804e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:09:55 +0200 Subject: [PATCH 216/333] add h3 to enum labels --- .../ftrack/event_handlers_user/action_prepare_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index eddad851e3..5f64adf920 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -246,7 +246,7 @@ class PrepareProjectLocal(BaseAction): multiselect_enumerators.append(self.item_splitter) multiselect_enumerators.append({ "type": "label", - "value": in_data["label"] + "value": "

{}

".format(in_data["label"]) }) default = in_data["default"] From ccce38eebbf1143b9f13d1889d261b9b2611e475 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:10:24 +0200 Subject: [PATCH 217/333] add create project structure checkbox --- .../action_prepare_project.py | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index 5f64adf920..c53303b7f9 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -24,7 +24,9 @@ class PrepareProjectLocal(BaseAction): settings_key = "prepare_project" - # Key to store info about trigerring create folder structure + # Key to store info about trigerring create folder structure\ + create_project_structure_key = "create_folder_structure" + create_project_structure_identifier = "create.project.structure" item_splitter = {"type": "label", "value": "---"} _keys_order = ( "fps", @@ -103,6 +105,27 @@ class PrepareProjectLocal(BaseAction): # Add autosync attribute items.append(auto_sync_item) + # This item will be last before enumerators + # Ask if want to trigger Action Create Folder Structure + create_project_structure_checked = ( + project_settings + ["project_settings"] + ["ftrack"] + ["user_handlers"] + ["prepare_project"] + ["create_project_structure_checked"] + ).value + items.append({ + "type": "label", + "value": "

Want to create basic Folder Structure?

" + }) + items.append({ + "name": self.create_project_structure_key, + "type": "boolean", + "value": create_project_structure_checked, + "label": "Check if Yes" + }) + # Add enumerator items at the end for item in multiselect_enumerators: items.append(item) @@ -307,10 +330,13 @@ class PrepareProjectLocal(BaseAction): return items, multiselect_enumerators def launch(self, session, entities, event): - if not event['data'].get('values', {}): + in_data = event["data"].get("values") + if not in_data: return - in_data = event['data']['values'] + create_project_structure_checked = in_data.pop( + self.create_project_structure_key + ) root_values = {} root_key = "__root__" @@ -400,6 +426,11 @@ class PrepareProjectLocal(BaseAction): self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) session.commit() + # Trigger create project structure action + if create_project_structure_checked: + self.trigger_action( + self.create_project_structure_identifier, event + ) return True From 699c3b5e060b9a4b1cf397b566c198427930af8e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:14:15 +0200 Subject: [PATCH 218/333] update server prepare project action with all changes --- .../action_prepare_project.py | 86 ++++++++++++++----- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py index 12d687bbf2..3a96ae3311 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py @@ -1,6 +1,8 @@ import json +from avalon.api import AvalonMongoDB from openpype.api import ProjectSettings +from openpype.lib import create_project from openpype.modules.ftrack.lib import ( ServerAction, @@ -21,8 +23,24 @@ class PrepareProjectServer(ServerAction): role_list = ["Pypeclub", "Administrator", "Project Manager"] - # Key to store info about trigerring create folder structure + settings_key = "prepare_project" + item_splitter = {"type": "label", "value": "---"} + _keys_order = ( + "fps", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "clipIn", + "clipOut", + "resolutionHeight", + "resolutionWidth", + "pixelAspect", + "applications", + "tools_env", + "library_project", + ) def discover(self, session, entities, event): """Show only on project.""" @@ -47,13 +65,7 @@ class PrepareProjectServer(ServerAction): project_entity = entities[0] project_name = project_entity["full_name"] - try: - project_settings = ProjectSettings(project_name) - except ValueError: - return { - "message": "Project is not synchronized yet", - "success": False - } + project_settings = ProjectSettings(project_name) project_anatom_settings = project_settings["project_anatomy"] root_items = self.prepare_root_items(project_anatom_settings) @@ -78,14 +90,13 @@ class PrepareProjectServer(ServerAction): items.extend(ca_items) - # This item will be last (before enumerators) - # - sets value of auto synchronization - auto_sync_name = "avalon_auto_sync" + # This item will be last before enumerators + # Set value of auto synchronization auto_sync_value = project_entity["custom_attributes"].get( CUST_ATTR_AUTO_SYNC, False ) auto_sync_item = { - "name": auto_sync_name, + "name": CUST_ATTR_AUTO_SYNC, "type": "boolean", "value": auto_sync_value, "label": "AutoSync to Avalon" @@ -199,7 +210,18 @@ class PrepareProjectServer(ServerAction): str([key for key in attributes_to_set]) )) - for key, in_data in attributes_to_set.items(): + attribute_keys = set(attributes_to_set.keys()) + keys_order = [] + for key in self._keys_order: + if key in attribute_keys: + keys_order.append(key) + + attribute_keys = attribute_keys - set(keys_order) + for key in sorted(attribute_keys): + keys_order.append(key) + + for key in keys_order: + in_data = attributes_to_set[key] attr = in_data["object"] # initial item definition @@ -225,7 +247,7 @@ class PrepareProjectServer(ServerAction): multiselect_enumerators.append(self.item_splitter) multiselect_enumerators.append({ "type": "label", - "value": in_data["label"] + "value": "

{}

".format(in_data["label"]) }) default = in_data["default"] @@ -286,10 +308,10 @@ class PrepareProjectServer(ServerAction): return items, multiselect_enumerators def launch(self, session, entities, event): - if not event['data'].get('values', {}): + in_data = event["data"].get("values") + if not in_data: return - in_data = event['data']['values'] root_values = {} root_key = "__root__" @@ -337,7 +359,27 @@ class PrepareProjectServer(ServerAction): self.log.debug("Setting Custom Attribute values") - project_name = entities[0]["full_name"] + project_entity = entities[0] + project_name = project_entity["full_name"] + + # Try to find project document + dbcon = AvalonMongoDB() + dbcon.install() + dbcon.Session["AVALON_PROJECT"] = project_name + project_doc = dbcon.find_one({ + "type": "project" + }) + # Create project if is not available + # - creation is required to be able set project anatomy and attributes + if not project_doc: + project_code = project_entity["name"] + self.log.info("Creating project \"{} [{}]\"".format( + project_name, project_code + )) + create_project(project_name, project_code, dbcon=dbcon) + + dbcon.uninstall() + project_settings = ProjectSettings(project_name) project_anatomy_settings = project_settings["project_anatomy"] project_anatomy_settings["roots"] = root_data @@ -352,10 +394,12 @@ class PrepareProjectServer(ServerAction): project_settings.save() - entity = entities[0] - for key, value in custom_attribute_values.items(): - entity["custom_attributes"][key] = value - self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + # Change custom attributes on project + if custom_attribute_values: + for key, value in custom_attribute_values.items(): + project_entity["custom_attributes"][key] = value + self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value)) + session.commit() return True From fcde4277e33422ffa2f67b656ad37d799925b25c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 11:15:17 +0200 Subject: [PATCH 219/333] removed slash from comment --- .../ftrack/event_handlers_user/action_prepare_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index c53303b7f9..ea0bfa2971 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -24,7 +24,7 @@ class PrepareProjectLocal(BaseAction): settings_key = "prepare_project" - # Key to store info about trigerring create folder structure\ + # Key to store info about trigerring create folder structure create_project_structure_key = "create_folder_structure" create_project_structure_identifier = "create.project.structure" item_splitter = {"type": "label", "value": "---"} From c2ffeb89538dc0fe845a98e78ec7ad34858ff7ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 27 Jul 2021 11:30:12 +0200 Subject: [PATCH 220/333] Textures publishing - tweaked validator Look for resources (secondary workfiles) only for main workfile. --- .../plugins/publish/validate_texture_workfiles.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py index 189246144d..aa3aad71db 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -14,8 +14,16 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): families = ["workfile"] optional = True + # from presets + main_workfile_extensions = ['mra'] + def process(self, instance): if instance.data["family"] == "workfile": - msg = "No resources for workfile {}".\ + ext = instance.data["representations"][0]["ext"] + if ext not in self.main_workfile_extensions: + self.log.warning("Only secondary workfile present!") + return + + msg = "No secondary workfiles present for workfile {}".\ format(instance.data["name"]) assert instance.data.get("resources"), msg From 281e6645ffcc7af8dc39f57e5f8d49fc6b34ae87 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 12:26:37 +0200 Subject: [PATCH 221/333] emit workfile arguments as list instead of path --- openpype/tools/workfiles/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index d567e26d74..f98085e579 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -695,14 +695,14 @@ class FilesWidget(QtWidgets.QWidget): file_path = os.path.join(self.root, work_file) - pipeline.emit("before.workfile.save", file_path) + pipeline.emit("before.workfile.save", [file_path]) self._enter_session() # Make sure we are in the right session self.host.save_file(file_path) self.set_asset_task(self._asset, self._task) - pipeline.emit("after.workfile.save", file_path) + pipeline.emit("after.workfile.save", [file_path]) self.workfile_created.emit(file_path) From ce301f8d0a2907eec5c2f5ad5916cb9a8f1cb7fc Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 27 Jul 2021 14:18:20 +0200 Subject: [PATCH 222/333] add support for RedshiftNormalMap node, fix tx linear space --- .../maya/plugins/publish/collect_look.py | 15 ++++++++++-- .../maya/plugins/publish/extract_look.py | 24 ++++++++++++------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index bf24b463ac..0dde52447d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -167,6 +167,8 @@ def get_file_node_path(node): if cmds.nodeType(node) == 'aiImage': return cmds.getAttr('{0}.filename'.format(node)) + if cmds.nodeType(node) == 'RedshiftNormalMap': + return cmds.getAttr('{}.tex0'.format(node)) # otherwise use fileTextureName return cmds.getAttr('{0}.fileTextureName'.format(node)) @@ -357,6 +359,7 @@ class CollectLook(pyblish.api.InstancePlugin): files = cmds.ls(history, type="file", long=True) files.extend(cmds.ls(history, type="aiImage", long=True)) + files.extend(cmds.ls(history, type="RedshiftNormalMap", long=True)) self.log.info("Collected file nodes:\n{}".format(files)) # Collect textures if any file nodes are found @@ -487,7 +490,7 @@ class CollectLook(pyblish.api.InstancePlugin): """ self.log.debug("processing: {}".format(node)) - if cmds.nodeType(node) not in ["file", "aiImage"]: + if cmds.nodeType(node) not in ["file", "aiImage", "RedshiftNormalMap"]: self.log.error( "Unsupported file node: {}".format(cmds.nodeType(node))) raise AssertionError("Unsupported file node") @@ -500,11 +503,19 @@ class CollectLook(pyblish.api.InstancePlugin): self.log.debug("aiImage node") attribute = "{}.filename".format(node) computed_attribute = attribute + elif cmds.nodeType(node) == 'RedshiftNormalMap': + self.log.debug("RedshiftNormalMap node") + attribute = "{}.tex0".format(node) + computed_attribute = attribute source = cmds.getAttr(attribute) self.log.info(" - file source: {}".format(source)) color_space_attr = "{}.colorSpace".format(node) - color_space = cmds.getAttr(color_space_attr) + try: + color_space = cmds.getAttr(color_space_attr) + except ValueError: + # node doesn't have colorspace attribute + color_space = "raw" # Compare with the computed file path, e.g. the one with the # pattern in it, to generate some logging information about this # difference diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index bdd061578e..c823602dc4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -233,11 +233,14 @@ class ExtractLook(openpype.api.Extractor): for filepath in files_metadata: linearize = False - if do_maketx and files_metadata[filepath]["color_space"] == "sRGB": # noqa: E501 + if do_maketx and files_metadata[filepath]["color_space"].lower() == "srgb": # noqa: E501 linearize = True # set its file node to 'raw' as tx will be linearized files_metadata[filepath]["color_space"] = "raw" + if do_maketx: + color_space = "raw" + source, mode, texture_hash = self._process_texture( filepath, do_maketx, @@ -280,15 +283,20 @@ class ExtractLook(openpype.api.Extractor): # This will also trigger in the same order at end of context to # ensure after context it's still the original value. color_space_attr = resource["node"] + ".colorSpace" - color_space = cmds.getAttr(color_space_attr) - if files_metadata[source]["color_space"] == "raw": - # set color space to raw if we linearized it - color_space = "Raw" - # Remap file node filename to destination + try: + color_space = cmds.getAttr(color_space_attr) + except ValueError: + # node doesn't have color space attribute + color_space = "raw" + else: + if files_metadata[source]["color_space"] == "raw": + # set color space to raw if we linearized it + color_space = "raw" + # Remap file node filename to destination + remap[color_space_attr] = color_space attr = resource["attribute"] remap[attr] = destinations[source] - remap[color_space_attr] = color_space - + self.log.info("Finished remapping destinations ...") # Extract in correct render layer From 834d6b681697c2cd93f862789a8ba5d2666f2a71 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 14:30:21 +0200 Subject: [PATCH 223/333] all anatomy children must be groups otherwise schema error is raised --- .../settings/entities/anatomy_entities.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/settings/entities/anatomy_entities.py b/openpype/settings/entities/anatomy_entities.py index d048ffabba..9edd0d943c 100644 --- a/openpype/settings/entities/anatomy_entities.py +++ b/openpype/settings/entities/anatomy_entities.py @@ -1,5 +1,6 @@ from .dict_immutable_keys_entity import DictImmutableKeysEntity from .lib import OverrideState +from .exceptions import EntitySchemaError class AnatomyEntity(DictImmutableKeysEntity): @@ -23,3 +24,22 @@ class AnatomyEntity(DictImmutableKeysEntity): if not child_obj.has_project_override: child_obj.add_to_project_override() return super(AnatomyEntity, self).on_child_change(child_obj) + + def schema_validations(self): + non_group_children = [] + for key, child_obj in self.non_gui_children.items(): + if not child_obj.is_group: + non_group_children.append(key) + + if non_group_children: + _non_group_children = [ + "project_anatomy/{}".format(key) + for key in non_group_children + ] + reason = ( + "Anatomy must have all children as groups." + " Non-group children {}" + ).format(", ".join(_non_group_children)) + raise EntitySchemaError(self, reason) + + return super(AnatomyEntity, self).schema_validations() From 13f6661a7c88bde185d6033159227c92da3c0891 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 14:32:26 +0200 Subject: [PATCH 224/333] added brief description to readme --- openpype/settings/entities/schemas/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index d457e44e74..e5122094f6 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -577,6 +577,15 @@ How output of the schema could look like on save: } ``` +## Anatomy +Anatomy represents data stored on project document. + +### anatomy +- entity works similarly to `dict` +- anatomy has always all keys overriden with overrides + - overrides are not applied as all anatomy data must be available from project document + - all children must be groups + ## Proxy wrappers - should wraps multiple inputs only visually - these does not have `"key"` key and do not allow to have `"is_file"` or `"is_group"` modifiers enabled From e97d14634535bed8aa2daaa4a3078dc6bf252427 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 14:50:38 +0200 Subject: [PATCH 225/333] added more specific name of template filename --- openpype/settings/entities/schemas/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 02e3e0a83c..8760187038 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -452,6 +452,8 @@ How output of the schema could look like on save: # WARNING: # In this example the template use itself inside which will work in `list` # but may cause an issue in other entity types (e.g. `dict`). + +'template_object_example.json' : [ { "type": "dict-conditional", From 74c74dc97d53d52536a82c2591102534aafb1d57 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 27 Jul 2021 15:13:31 +0200 Subject: [PATCH 226/333] Settings: adding workfile tool start attribute --- .../defaults/project_settings/global.json | 7 ++++ .../schemas/schema_global_tools.json | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 43053c38c0..636acc0d17 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -260,6 +260,13 @@ "enabled": true } ], + "open_workfile_tool_on_startup": [ + { + "hosts": [], + "tasks": [], + "enabled": false + } + ], "sw_folders": { "compositing": [ "nuke", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 8c92a45a56..a9fe27c24b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -97,6 +97,38 @@ ] } }, + { + "type": "list", + "key": "open_workfile_tool_on_startup", + "label": "Open workfile tool on launch", + "is_group": true, + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "hosts-enum", + "key": "hosts", + "label": "Hosts", + "multiselection": true + }, + { + "key": "tasks", + "label": "Tasks", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + } + }, { "type": "dict-modifiable", "collapsible": true, From 08b0b3035a02d1a41274a8b0e9b4f64f419b5434 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 27 Jul 2021 15:14:03 +0200 Subject: [PATCH 227/333] global: adding workfile start at launch attribute search func --- openpype/lib/applications.py | 119 +++++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 33 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e1b304a351..01bc0cddf8 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1302,10 +1302,18 @@ def _prepare_last_workfile(data, workdir): ) data["start_last_workfile"] = start_last_workfile + workfile_startup = should_workfile_tool_start( + project_name, app.host_name, task_name + ) + data["workfile_startup"] = workfile_startup + # Store boolean as "0"(False) or "1"(True) data["env"]["AVALON_OPEN_LAST_WORKFILE"] = ( str(int(bool(start_last_workfile))) ) + data["env"]["WORKFILE_STARTUP"] = ( + str(int(bool(workfile_startup))) + ) _sub_msg = "" if start_last_workfile else " not" log.debug( @@ -1344,40 +1352,9 @@ def _prepare_last_workfile(data, workdir): data["last_workfile_path"] = last_workfile_path -def should_start_last_workfile( - project_name, host_name, task_name, default_output=False +def get_option_from_preset( + startup_presets, host_name, task_name, default_output ): - """Define if host should start last version workfile if possible. - - Default output is `False`. Can be overriden with environment variable - `AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are - `"0", "1", "true", "false", "yes", "no"`. - - Args: - project_name (str): Name of project. - host_name (str): Name of host which is launched. In avalon's - application context it's value stored in app definition under - key `"application_dir"`. Is not case sensitive. - task_name (str): Name of task which is used for launching the host. - Task name is not case sensitive. - - Returns: - bool: True if host should start workfile. - - """ - - project_settings = get_project_settings(project_name) - startup_presets = ( - project_settings - ["global"] - ["tools"] - ["Workfiles"] - ["last_workfile_on_startup"] - ) - - if not startup_presets: - return default_output - host_name_lowered = host_name.lower() task_name_lowered = task_name.lower() @@ -1421,6 +1398,82 @@ def should_start_last_workfile( return default_output +def should_start_last_workfile( + project_name, host_name, task_name, default_output=False +): + """Define if host should start last version workfile if possible. + + Default output is `False`. Can be overriden with environment variable + `AVALON_OPEN_LAST_WORKFILE`, valid values without case sensitivity are + `"0", "1", "true", "false", "yes", "no"`. + + Args: + project_name (str): Name of project. + host_name (str): Name of host which is launched. In avalon's + application context it's value stored in app definition under + key `"application_dir"`. Is not case sensitive. + task_name (str): Name of task which is used for launching the host. + Task name is not case sensitive. + + Returns: + bool: True if host should start workfile. + + """ + + project_settings = get_project_settings(project_name) + startup_presets = ( + project_settings + ["global"] + ["tools"] + ["Workfiles"] + ["last_workfile_on_startup"] + ) + + if not startup_presets: + return default_output + + return get_option_from_preset( + startup_presets, host_name, task_name, default_output) + + +def should_workfile_tool_start( + project_name, host_name, task_name, default_output=False +): + """Define if host should start workfile tool at host launch. + + Default output is `False`. Can be overriden with environment variable + `WORKFILE_STARTUP`, valid values without case sensitivity are + `"0", "1", "true", "false", "yes", "no"`. + + Args: + project_name (str): Name of project. + host_name (str): Name of host which is launched. In avalon's + application context it's value stored in app definition under + key `"application_dir"`. Is not case sensitive. + task_name (str): Name of task which is used for launching the host. + Task name is not case sensitive. + + Returns: + bool: True if host should start workfile. + + """ + + project_settings = get_project_settings(project_name) + startup_presets = ( + project_settings + ["global"] + ["tools"] + ["Workfiles"] + ["open_workfile_tool_on_startup"] + ) + + if not startup_presets: + return default_output + + return get_option_from_preset( + startup_presets, host_name, task_name, default_output) + + def compile_list_of_regexes(in_list): """Convert strings in entered list to compiled regex objects.""" regexes = list() From d23f31da7edf5723d571a774e200f0056bbb0032 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 27 Jul 2021 15:14:22 +0200 Subject: [PATCH 228/333] Nuke: refactory workfile launch callback --- openpype/hosts/nuke/api/lib.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index eefbcc5d20..fce92f08d5 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1660,9 +1660,13 @@ def find_free_space_to_paste_nodes( def launch_workfiles_app(): '''Function letting start workfiles after start of host ''' - # get state from settings - open_at_start = get_current_project_settings()["nuke"].get( - "general", {}).get("open_workfile_at_start") + from openpype.lib import ( + env_value_to_bool + ) + # get all imortant settings + open_at_start = env_value_to_bool( + env_key="WORKFILE_STARTUP", + default=None) # return if none is defined if not open_at_start: From bee136e29a3fbcdd0d24438e3cfda47d3eebf3fd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 15:22:11 +0200 Subject: [PATCH 229/333] added process identifier to base handler --- openpype/modules/ftrack/lib/ftrack_base_handler.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index 011ce8db9d..b8be287a03 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -2,6 +2,7 @@ import os import tempfile import json import functools +import uuid import datetime import traceback import time @@ -36,6 +37,7 @@ class BaseHandler(object): - a verbose descriptive text for you action - icon in ftrack ''' + _process_id = None # Default priority is 100 priority = 100 # Type is just for logging purpose (e.g.: Action, Event, Application,...) @@ -70,6 +72,13 @@ class BaseHandler(object): self.register = self.register_decorator(self.register) self.launch = self.launch_log(self.launch) + @staticmethod + def process_identifier(): + """Helper property to have """ + if not BaseHandler._process_id: + BaseHandler._process_id = str(uuid.uuid4()) + return BaseHandler._process_id + # Decorator def register_decorator(self, func): @functools.wraps(func) From 7737dbb326d26ae2e25f3b037f7b814d0f2cf6d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 15:22:58 +0200 Subject: [PATCH 230/333] use process_identifier instead of uuid for each action --- openpype/modules/ftrack/lib/ftrack_action_handler.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 878eac6627..1c9faec6bf 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -1,5 +1,4 @@ import os -from uuid import uuid4 from .ftrack_base_handler import BaseHandler @@ -30,7 +29,6 @@ class BaseAction(BaseHandler): icon = None type = 'Action' - _identifier_id = str(uuid4()) _discover_identifier = None _launch_identifier = None @@ -51,7 +49,7 @@ class BaseAction(BaseHandler): def discover_identifier(self): if self._discover_identifier is None: self._discover_identifier = "{}.{}".format( - self.identifier, self._identifier_id + self.identifier, self.process_identifier() ) return self._discover_identifier @@ -59,7 +57,7 @@ class BaseAction(BaseHandler): def launch_identifier(self): if self._launch_identifier is None: self._launch_identifier = "{}.{}".format( - self.identifier, self._identifier_id + self.identifier, self.process_identifier() ) return self._launch_identifier From 72a2bdfd04e5d060057ddbb28f5c4458db612b13 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 15:23:43 +0200 Subject: [PATCH 231/333] AppplicationsAction is using process identifier --- .../action_applications.py | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_applications.py b/openpype/modules/ftrack/event_handlers_user/action_applications.py index 58ea3c5671..74d14c2fc4 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_applications.py +++ b/openpype/modules/ftrack/event_handlers_user/action_applications.py @@ -11,21 +11,14 @@ from avalon.api import AvalonMongoDB class AppplicationsAction(BaseAction): - """Application Action class. - - Args: - session (ftrack_api.Session): Session where action will be registered. - label (str): A descriptive string identifing your action. - varaint (str, optional): To group actions together, give them the same - label and specify a unique variant per action. - identifier (str): An unique identifier for app. - description (str): A verbose descriptive text for you action. - icon (str): Url path to icon which will be shown in Ftrack web. - """ + """Applications Action class.""" type = "Application" label = "Application action" - identifier = "pype_app.{}.".format(str(uuid4())) + + identifier = "openpype_app" + _launch_identifier_with_id = None + icon_url = os.environ.get("OPENPYPE_STATICS_SERVER") def __init__(self, *args, **kwargs): @@ -34,6 +27,28 @@ class AppplicationsAction(BaseAction): self.application_manager = ApplicationManager() self.dbcon = AvalonMongoDB() + @property + def discover_identifier(self): + if self._discover_identifier is None: + self._discover_identifier = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._discover_identifier + + @property + def launch_identifier(self): + if self._launch_identifier is None: + self._launch_identifier = "{}.*".format(self.identifier) + return self._launch_identifier + + @property + def launch_identifier_with_id(self): + if self._launch_identifier_with_id is None: + self._launch_identifier_with_id = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._launch_identifier_with_id + def construct_requirements_validations(self): # Override validation as this action does not need them return @@ -56,7 +71,7 @@ class AppplicationsAction(BaseAction): " and data.actionIdentifier={0}" " and source.user.username={1}" ).format( - self.identifier + "*", + self.launch_identifier, self.session.api_user ) self.session.event_hub.subscribe( @@ -136,12 +151,29 @@ class AppplicationsAction(BaseAction): "label": app.group.label, "variant": app.label, "description": None, - "actionIdentifier": self.identifier + app_name, + "actionIdentifier": "{}.{}".format( + self.launch_identifier_with_id, app_name + ), "icon": app_icon }) return items + def _launch(self, event): + event_identifier = event["data"]["actionIdentifier"] + # Check if identifier is same + # - show message that acion may not be triggered on this machine + if event_identifier.startswith(self.launch_identifier_with_id): + return BaseAction._launch(self, event) + + return { + "success": False, + "message": ( + "There are running more OpenPype processes" + " where Application can be launched." + ) + } + def launch(self, session, entities, event): """Callback method for the custom action. @@ -162,7 +194,8 @@ class AppplicationsAction(BaseAction): *event* the unmodified original event """ identifier = event["data"]["actionIdentifier"] - app_name = identifier[len(self.identifier):] + id_identifier_len = len(self.launch_identifier_with_id) + 1 + app_name = identifier[id_identifier_len:] entity = entities[0] From 8ebdbd4f932ad1171257f40a754c9b04d3dc2a7b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 15:25:50 +0200 Subject: [PATCH 232/333] added helper LocalAction as base which tells user that is not launched because was launched in other process --- .../ftrack/lib/ftrack_action_handler.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 1c9faec6bf..b24fe5f12a 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -328,6 +328,78 @@ class BaseAction(BaseHandler): return True +class LocalAction(BaseAction): + """Action that warn user when more Processes with same action are running. + + Action is launched all the time but if id does not match id of current + instanace then message is shown to user. + + Handy for actions where matters if is executed on specific machine. + """ + _full_launch_identifier = None + + @property + def discover_identifier(self): + if self._discover_identifier is None: + self._discover_identifier = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._discover_identifier + + @property + def launch_identifier(self): + """Catch all topics with same identifier.""" + if self._launch_identifier is None: + self._launch_identifier = "{}.*".format(self.identifier) + return self._launch_identifier + + @property + def full_launch_identifier(self): + """Catch all topics with same identifier.""" + if self._full_launch_identifier is None: + self._full_launch_identifier = "{}.{}".format( + self.identifier, self.process_identifier() + ) + return self._full_launch_identifier + + def _discover(self, event): + entities = self._translate_event(event) + if not entities: + return + + accepts = self.discover(self.session, entities, event) + if not accepts: + return + + self.log.debug("Discovering action with selection: {0}".format( + event["data"].get("selection", []) + )) + + return { + "items": [{ + "label": self.label, + "variant": self.variant, + "description": self.description, + "actionIdentifier": self.discover_identifier, + "icon": self.icon, + }] + } + + def _launch(self, event): + event_identifier = event["data"]["actionIdentifier"] + # Check if identifier is same + # - show message that acion may not be triggered on this machine + if event_identifier != self.full_launch_identifier: + return { + "success": False, + "message": ( + "There are running more OpenPype processes" + " where this action could be launched." + ) + } + return super(LocalAction, self)._launch(event) + + class ServerAction(BaseAction): """Action class meant to be used on event server. From b911f3f0655551a829f5f15ec4efc199f4720e44 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 15:32:04 +0200 Subject: [PATCH 233/333] fix prepare project which triggers different action --- .../ftrack/event_handlers_user/action_prepare_project.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py index ea0bfa2971..4b42500e8f 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py +++ b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py @@ -428,9 +428,11 @@ class PrepareProjectLocal(BaseAction): # Trigger create project structure action if create_project_structure_checked: - self.trigger_action( - self.create_project_structure_identifier, event + trigger_identifier = "{}.{}".format( + self.create_project_structure_identifier, + self.process_identifier() ) + self.trigger_action(trigger_identifier, event) return True From a4089715b456c0700aa04dee9d4bd51ff6efedf6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 15:42:12 +0200 Subject: [PATCH 234/333] fix where I run action --- .../ftrack/event_handlers_user/action_where_run_show.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py b/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py index 4ce1a439a3..b8b49e86cb 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py +++ b/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py @@ -24,6 +24,10 @@ class ActionShowWhereIRun(BaseAction): return False + @property + def launch_identifier(self): + return self.identifier + def launch(self, session, entities, event): # Don't show info when was launch from this session if session.event_hub.id == event.get("data", {}).get("event_hub_id"): From 7a9fb009e3f6e857a7d35e1f60c7000a668e644a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:37:24 +0200 Subject: [PATCH 235/333] define host names in class definition --- openpype/settings/entities/enum_entity.py | 34 ++++++++++++----------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index d306eca7ef..8055b0167f 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,3 +1,4 @@ +import copy from .input_entities import InputEntity from .exceptions import EntitySchemaError from .lib import ( @@ -118,6 +119,22 @@ class HostsEnumEntity(BaseEnumEntity): implementation instead of application name. """ schema_types = ["hosts-enum"] + all_host_names = [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal", + "standalonepublisher" + ] def _item_initalization(self): self.multiselection = self.schema_data.get("multiselection", True) @@ -126,22 +143,7 @@ class HostsEnumEntity(BaseEnumEntity): ) custom_labels = self.schema_data.get("custom_labels") or {} - host_names = [ - "aftereffects", - "blender", - "celaction", - "fusion", - "harmony", - "hiero", - "houdini", - "maya", - "nuke", - "photoshop", - "resolve", - "tvpaint", - "unreal", - "standalonepublisher" - ] + host_names = copy.deepcopy(self.all_host_names) if self.use_empty_value: host_names.insert(0, "") # Add default label for empty value if not available From e450dc1254c4da191c258480d6a71f3fa5a2d555 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:37:30 +0200 Subject: [PATCH 236/333] use_empty_value can't be set if multiselection is used --- openpype/settings/entities/enum_entity.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 8055b0167f..f223898f83 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -138,9 +138,12 @@ class HostsEnumEntity(BaseEnumEntity): def _item_initalization(self): self.multiselection = self.schema_data.get("multiselection", True) - self.use_empty_value = self.schema_data.get( - "use_empty_value", not self.multiselection - ) + use_empty_value = False + if not self.multiselection: + use_empty_value = self.schema_data.get( + "use_empty_value", use_empty_value + ) + self.use_empty_value = use_empty_value custom_labels = self.schema_data.get("custom_labels") or {} host_names = copy.deepcopy(self.all_host_names) From caa7ff4993d5f0c446472b11d7005e26f394c9bc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:37:56 +0200 Subject: [PATCH 237/333] added hosts_filter attribute to explicitly filter available host names --- openpype/settings/entities/enum_entity.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index f223898f83..a712d71806 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -144,9 +144,18 @@ class HostsEnumEntity(BaseEnumEntity): "use_empty_value", use_empty_value ) self.use_empty_value = use_empty_value + + hosts_filter = self.schema_data.get("hosts_filter") or [] + self.hosts_filter = hosts_filter + custom_labels = self.schema_data.get("custom_labels") or {} host_names = copy.deepcopy(self.all_host_names) + if hosts_filter: + for host_name in tuple(host_names): + if host_name not in hosts_filter: + host_names.remove(host_name) + if self.use_empty_value: host_names.insert(0, "") # Add default label for empty value if not available From 916262da41242ec1dfbf8e8a99857dcccfe48bcd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:38:23 +0200 Subject: [PATCH 238/333] added schema validations for hosts filter --- openpype/settings/entities/enum_entity.py | 38 +++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index a712d71806..4f6a2886bc 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -187,6 +187,44 @@ class HostsEnumEntity(BaseEnumEntity): # GUI attribute self.placeholder = self.schema_data.get("placeholder") + def schema_validations(self): + if self.hosts_filter: + enum_len = len(self.enum_items) + if ( + enum_len == 0 + or (enum_len == 1 and self.use_empty_value) + ): + joined_filters = ", ".join([ + '"{}"'.format(item) + for item in self.hosts_filter + ]) + reason = ( + "All host names were removed after applying" + " host filters. {}" + ).format(joined_filters) + raise EntitySchemaError(self, reason) + + invalid_filters = set() + for item in self.hosts_filter: + if item not in self.all_host_names: + invalid_filters.add(item) + + if invalid_filters: + joined_filters = ", ".join([ + '"{}"'.format(item) + for item in self.hosts_filter + ]) + expected_hosts = ", ".join([ + '"{}"'.format(item) + for item in self.all_host_names + ]) + self.log.warning(( + "Host filters containt invalid host names:" + " \"{}\" Expected values are {}" + ).format(joined_filters, expected_hosts)) + + super(HostsEnumEntity, self).schema_validations() + class AppsEnumEntity(BaseEnumEntity): schema_types = ["apps-enum"] From 6d6f355e000f117be98407eead47192340a18661 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:38:42 +0200 Subject: [PATCH 239/333] added hosts_filter for workfiles on startup --- .../schemas/schema_global_tools.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 8c92a45a56..02ce8d6e88 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -78,7 +78,21 @@ "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "multiselection": true + "multiselection": true, + "hosts_filter": [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint" + ] }, { "key": "tasks", From dbfd8bff2f7e786a1691a108b11409773232d1e7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:41:21 +0200 Subject: [PATCH 240/333] added docs to readme --- openpype/settings/entities/schemas/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index d457e44e74..fae9b390fd 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -379,6 +379,9 @@ How output of the schema could look like on save: - multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) - it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) - it is possible to set `"custom_labels"` for host names where key `""` is empty value (Default: `{}`) +- to filter host names it is required to define `"hosts_filter"` which is list of host names that will be available + - do not pass empty string if `use_empty_value` is enabled + - ignoring host names would be more dangerous in some cases ``` { "key": "host", @@ -389,7 +392,10 @@ How output of the schema could look like on save: "custom_labels": { "": "N/A", "nuke": "Nuke" - } + }, + "hosts_filter": [ + "nuke" + ] } ``` From ac55f02fb6ed8198bb516398b3386dab451682c2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:46:35 +0200 Subject: [PATCH 241/333] added nreal back to hosts filter --- .../schemas/projects_schema/schemas/schema_global_tools.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 02ce8d6e88..fa0e705cbf 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -91,7 +91,8 @@ "nuke", "photoshop", "resolve", - "tvpaint" + "tvpaint", + "unreal" ] }, { From 3f97ee17b3d141663576aa238a70affef10a89e4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 16:53:10 +0200 Subject: [PATCH 242/333] modified error message --- openpype/settings/entities/anatomy_entities.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/anatomy_entities.py b/openpype/settings/entities/anatomy_entities.py index 9edd0d943c..489e1f8294 100644 --- a/openpype/settings/entities/anatomy_entities.py +++ b/openpype/settings/entities/anatomy_entities.py @@ -38,8 +38,11 @@ class AnatomyEntity(DictImmutableKeysEntity): ] reason = ( "Anatomy must have all children as groups." - " Non-group children {}" - ).format(", ".join(_non_group_children)) + " Set 'is_group' to `true` on > {}" + ).format(", ".join([ + '"{}"'.format(item) + for item in _non_group_children + ])) raise EntitySchemaError(self, reason) return super(AnatomyEntity, self).schema_validations() From f1ef07c7d1e128a01660227e12475999afc7abdb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 11:26:05 +0200 Subject: [PATCH 243/333] fix receivers discovery --- openpype/tools/settings/settings/window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index a60a2a1d88..54f8ec0a11 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -141,7 +141,10 @@ class MainWidget(QtWidgets.QWidget): # Don't show dialog if there are not registered slots for # `trigger_restart` signal. # - For example when settings are runnin as standalone tool - if self.receivers(self.trigger_restart) < 1: + # - PySide2 and PyQt5 compatible way how to find out + method_index = self.metaObject().indexOfMethod("trigger_restart()") + method = self.metaObject().method(method_index) + if not self.isSignalConnected(method): return dialog = RestartDialog(self) From e7a3b0633aad0b0e8185a0aca78334cdfa86281f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 11:56:56 +0200 Subject: [PATCH 244/333] fix log viewer stylesheet of qtoolbutton --- openpype/modules/log_viewer/tray/app.py | 8 ++++---- openpype/modules/log_viewer/tray/widgets.py | 5 ++--- openpype/style/style.css | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/modules/log_viewer/tray/app.py b/openpype/modules/log_viewer/tray/app.py index 9aab37cd20..1e8d6483cd 100644 --- a/openpype/modules/log_viewer/tray/app.py +++ b/openpype/modules/log_viewer/tray/app.py @@ -7,12 +7,13 @@ class LogsWindow(QtWidgets.QWidget): def __init__(self, parent=None): super(LogsWindow, self).__init__(parent) - self.setStyleSheet(style.load_stylesheet()) + self.setWindowTitle("Logs viewer") + self.resize(1400, 800) log_detail = OutputWidget(parent=self) logs_widget = LogsWidget(log_detail, parent=self) - main_layout = QtWidgets.QHBoxLayout() + main_layout = QtWidgets.QHBoxLayout(self) log_splitter = QtWidgets.QSplitter(self) log_splitter.setOrientation(QtCore.Qt.Horizontal) @@ -24,5 +25,4 @@ class LogsWindow(QtWidgets.QWidget): self.logs_widget = logs_widget self.log_detail = log_detail - self.setLayout(main_layout) - self.setWindowTitle("Logs") + self.setStyleSheet(style.load_stylesheet()) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index b9a8499a4c..d906a1b6ad 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -76,13 +76,12 @@ class CustomCombo(QtWidgets.QWidget): toolbutton.setMenu(toolmenu) toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) + toolbutton.setProperty("popup_mode", "1") - layout = QtWidgets.QHBoxLayout() + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(toolbutton) - self.setLayout(layout) - toolmenu.selection_changed.connect(self.selection_changed) self.toolbutton = toolbutton diff --git a/openpype/style/style.css b/openpype/style/style.css index c57b9a8da6..8391fcd0ae 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -97,7 +97,7 @@ QToolButton:disabled { background: {color:bg-buttons-disabled}; } -QToolButton[popupMode="1"] { +QToolButton[popupMode="1"], QToolButton[popup_mode="1"] { /* make way for the popup button */ padding-right: 20px; border: 1px solid {color:bg-buttons}; From ae20e682f839a5595b879333a4430676c0f8a203 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 12:11:17 +0200 Subject: [PATCH 245/333] added comment --- openpype/modules/log_viewer/tray/widgets.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index d906a1b6ad..669acf4b67 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -76,6 +76,9 @@ class CustomCombo(QtWidgets.QWidget): toolbutton.setMenu(toolmenu) toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) + + # Fake popupMenu property as PySide2 does not store it's value as + # integer but as enum object toolbutton.setProperty("popup_mode", "1") layout = QtWidgets.QHBoxLayout(self) From 16a258bc2764956dd3bfdc2eedc2d90416ca3761 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 12:38:50 +0200 Subject: [PATCH 246/333] fixed popupMode property --- openpype/modules/log_viewer/tray/widgets.py | 4 ---- openpype/style/style.css | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index 669acf4b67..0f77a7f111 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -77,10 +77,6 @@ class CustomCombo(QtWidgets.QWidget): toolbutton.setMenu(toolmenu) toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup) - # Fake popupMenu property as PySide2 does not store it's value as - # integer but as enum object - toolbutton.setProperty("popup_mode", "1") - layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(toolbutton) diff --git a/openpype/style/style.css b/openpype/style/style.css index 8391fcd0ae..8dffd98e43 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -97,7 +97,7 @@ QToolButton:disabled { background: {color:bg-buttons-disabled}; } -QToolButton[popupMode="1"], QToolButton[popup_mode="1"] { +QToolButton[popupMode="1"], QToolButton[popupMode="MenuButtonPopup"] { /* make way for the popup button */ padding-right: 20px; border: 1px solid {color:bg-buttons}; From 65be35d86c6126d5a4b38ade3317fb9bb19fe613 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 10:15:49 +0200 Subject: [PATCH 247/333] use parenting to skip style set --- .../tools/standalonepublish/widgets/widget_component_item.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_component_item.py b/openpype/tools/standalonepublish/widgets/widget_component_item.py index 186c8024db..de3cde50cd 100644 --- a/openpype/tools/standalonepublish/widgets/widget_component_item.py +++ b/openpype/tools/standalonepublish/widgets/widget_component_item.py @@ -1,7 +1,6 @@ import os from Qt import QtCore, QtGui, QtWidgets from .resources import get_resource -from avalon import style class ComponentItem(QtWidgets.QFrame): @@ -61,7 +60,7 @@ class ComponentItem(QtWidgets.QFrame): name="menu", size=QtCore.QSize(22, 22) ) - self.action_menu = QtWidgets.QMenu() + self.action_menu = QtWidgets.QMenu(self.btn_action_menu) expanding_sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding @@ -229,7 +228,6 @@ class ComponentItem(QtWidgets.QFrame): if not self.btn_action_menu.isVisible(): self.btn_action_menu.setVisible(True) self.btn_action_menu.clicked.connect(self.show_actions) - self.action_menu.setStyleSheet(style.load_stylesheet()) def set_repre_name_valid(self, valid): self.has_valid_repre = valid From 324560a6e93c2a7a2c3de2f2f51e3452eb7e3a20 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 27 Jul 2021 17:41:21 +0200 Subject: [PATCH 248/333] hide outlines of selected item --- openpype/modules/log_viewer/tray/widgets.py | 5 ++--- openpype/style/style.css | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index 0f77a7f111..5a67780413 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -139,7 +139,6 @@ class LogsWidget(QtWidgets.QWidget): filter_layout.addWidget(refresh_btn) view = QtWidgets.QTreeView(self) - view.setAllColumnsShowFocus(True) view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) layout = QtWidgets.QVBoxLayout(self) @@ -227,9 +226,9 @@ class OutputWidget(QtWidgets.QWidget): super(OutputWidget, self).__init__(parent=parent) layout = QtWidgets.QVBoxLayout(self) - show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp") + show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp", self) - output_text = QtWidgets.QTextEdit() + output_text = QtWidgets.QTextEdit(self) output_text.setReadOnly(True) # output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth) diff --git a/openpype/style/style.css b/openpype/style/style.css index 8dffd98e43..12ea960859 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -340,6 +340,11 @@ QAbstractItemView { selection-background-color: transparent; } +QAbstractItemView::item { + /* `border: none` hide outline of selected item. */ + border: none; +} + QAbstractItemView:disabled{ background: {color:bg-view-disabled}; alternate-background-color: {color:bg-view-alternate-disabled}; From f706c43bf7c6619c70cb9d645c02c3f0a22e6a0e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 28 Jul 2021 03:42:04 +0000 Subject: [PATCH 249/333] [Automated] Bump version --- CHANGELOG.md | 35 +++++++++++++---------------------- openpype/version.py | 2 +- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f75f68a5bd..fbd5ccd412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,18 @@ # Changelog -## [3.3.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) **🚀 Enhancements** +- Anatomy schema validation [\#1864](https://github.com/pypeclub/OpenPype/pull/1864) +- Ftrack prepare project structure [\#1861](https://github.com/pypeclub/OpenPype/pull/1861) +- Independent general environments [\#1853](https://github.com/pypeclub/OpenPype/pull/1853) +- TVPaint Start Frame [\#1844](https://github.com/pypeclub/OpenPype/pull/1844) - Ftrack push attributes action adds traceback to job [\#1843](https://github.com/pypeclub/OpenPype/pull/1843) - Prepare project action enhance [\#1838](https://github.com/pypeclub/OpenPype/pull/1838) +- Standalone Publish of textures family [\#1834](https://github.com/pypeclub/OpenPype/pull/1834) - nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829) - Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823) - Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) @@ -15,17 +20,23 @@ **🐛 Bug fixes** +- imageio: fix grouping [\#1856](https://github.com/pypeclub/OpenPype/pull/1856) +- publisher: missing version in subset prop [\#1849](https://github.com/pypeclub/OpenPype/pull/1849) +- Ftrack type error fix in sync to avalon event handler [\#1845](https://github.com/pypeclub/OpenPype/pull/1845) - Nuke: updating effects subset fail [\#1841](https://github.com/pypeclub/OpenPype/pull/1841) - nuke: write render node skipped with crop [\#1836](https://github.com/pypeclub/OpenPype/pull/1836) - Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) - Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Failsafe for cross project containers. [\#1806](https://github.com/pypeclub/OpenPype/pull/1806) +- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) - Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) **Merged pull requests:** +- Ftrack push attributes action adds traceback to job [\#1842](https://github.com/pypeclub/OpenPype/pull/1842) - Add support for pyenv-win on windows [\#1822](https://github.com/pypeclub/OpenPype/pull/1822) - PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811) +- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808) ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) @@ -47,11 +58,9 @@ - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) -- Toggle Ftrack upload in StandalonePublisher [\#1708](https://github.com/pypeclub/OpenPype/pull/1708) **🐛 Bug fixes** -- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) - Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) - Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) - Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) @@ -73,9 +82,9 @@ **Merged pull requests:** -- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808) - Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) - Bc/fix/docs [\#1771](https://github.com/pypeclub/OpenPype/pull/1771) +- Expose write attributes to config [\#1770](https://github.com/pypeclub/OpenPype/pull/1770) - TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) @@ -95,10 +104,6 @@ - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) -**⚠️ Deprecations** - -- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) - ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2) @@ -107,24 +112,10 @@ - Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713) -**Merged pull requests:** - -- 1698 Nuke: Prerender Frame Range by default [\#1709](https://github.com/pypeclub/OpenPype/pull/1709) - ## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.1.0-nightly.4...3.1.0) -**🚀 Enhancements** - -- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) -- Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) - -**🐛 Bug fixes** - -- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) -- Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - # Changelog diff --git a/openpype/version.py b/openpype/version.py index 55f4c21997..d7efcf6bd5 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.4" +__version__ = "3.3.0-nightly.5" From 5c6b15b30aaf73e7cf7ec07e14bd0703388d12e2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Jul 2021 10:32:17 +0200 Subject: [PATCH 250/333] skip entities that are None after query --- .../modules/ftrack/lib/ftrack_base_handler.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py index 011ce8db9d..cfe55014a8 100644 --- a/openpype/modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_base_handler.py @@ -182,15 +182,22 @@ class BaseHandler(object): if session is None: session = self.session - _entities = event['data'].get('entities_object', None) + _entities = event["data"].get("entities_object", None) + if _entities is not None and not _entities: + return _entities + if ( - _entities is None or - _entities[0].get( - 'link', None + _entities is None + or _entities[0].get( + "link", None ) == ftrack_api.symbol.NOT_SET ): - _entities = self._get_entities(event) - event['data']['entities_object'] = _entities + _entities = [ + item + for item in self._get_entities(event) + if item is not None + ] + event["data"]["entities_object"] = _entities return _entities From 23ee92ecb974c1f8c51ad9a170f38b4f9792af61 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 28 Jul 2021 10:52:15 +0200 Subject: [PATCH 251/333] labels are transparent by default --- openpype/style/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index 12ea960859..b955bdc2a6 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -35,6 +35,10 @@ QWidget:disabled { color: {color:font-disabled}; } +QLabel { + background: transparent; +} + /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; From b4c27aa5d28488b37ad5a9578aa8caed6e17ff24 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 28 Jul 2021 11:11:46 +0100 Subject: [PATCH 252/333] Update pre_copy_template_workfile.py Spelling correction --- openpype/hooks/pre_copy_template_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_copy_template_workfile.py b/openpype/hooks/pre_copy_template_workfile.py index 29a522f933..5c56d721e8 100644 --- a/openpype/hooks/pre_copy_template_workfile.py +++ b/openpype/hooks/pre_copy_template_workfile.py @@ -49,7 +49,7 @@ class CopyTemplateWorkfile(PreLaunchHook): )) return - self.log.info("Last workfile does not exits.") + self.log.info("Last workfile does not exist.") project_name = self.data["project_name"] asset_name = self.data["asset_name"] From 6a2bd167b5fdadf6283dbfdbc783cb44c1efbab4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 28 Jul 2021 14:08:41 +0200 Subject: [PATCH 253/333] global: better env var name --- openpype/lib/applications.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 01bc0cddf8..ada194f15f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1311,7 +1311,7 @@ def _prepare_last_workfile(data, workdir): data["env"]["AVALON_OPEN_LAST_WORKFILE"] = ( str(int(bool(start_last_workfile))) ) - data["env"]["WORKFILE_STARTUP"] = ( + data["env"]["OPENPYPE_WORKFILE_TOOL_ON_START"] = ( str(int(bool(workfile_startup))) ) @@ -1352,7 +1352,7 @@ def _prepare_last_workfile(data, workdir): data["last_workfile_path"] = last_workfile_path -def get_option_from_preset( +def get_option_from_settings( startup_presets, host_name, task_name, default_output ): host_name_lowered = host_name.lower() @@ -1432,7 +1432,7 @@ def should_start_last_workfile( if not startup_presets: return default_output - return get_option_from_preset( + return get_option_from_settings( startup_presets, host_name, task_name, default_output) @@ -1442,7 +1442,7 @@ def should_workfile_tool_start( """Define if host should start workfile tool at host launch. Default output is `False`. Can be overriden with environment variable - `WORKFILE_STARTUP`, valid values without case sensitivity are + `OPENPYPE_WORKFILE_TOOL_ON_START`, valid values without case sensitivity are `"0", "1", "true", "false", "yes", "no"`. Args: @@ -1470,7 +1470,7 @@ def should_workfile_tool_start( if not startup_presets: return default_output - return get_option_from_preset( + return get_option_from_settings( startup_presets, host_name, task_name, default_output) From 95b7ee57ec0952c6bcaa829ec63ab0d36bb6032a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 28 Jul 2021 14:09:11 +0200 Subject: [PATCH 254/333] settings: add filter to host which are supported now --- .../schemas/projects_schema/schemas/schema_global_tools.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 211a8d0057..9e39eeb39e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -125,7 +125,10 @@ "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "multiselection": true + "multiselection": true, + "hosts_filter": [ + "nuke" + ] }, { "key": "tasks", From 9f018cb6fb840551fbb084eb263b3cf96dc00a94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 28 Jul 2021 14:09:29 +0200 Subject: [PATCH 255/333] nuke: improving env var name --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 442c0122be..7e7cd27f90 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -1665,7 +1665,7 @@ def launch_workfiles_app(): ) # get all imortant settings open_at_start = env_value_to_bool( - env_key="WORKFILE_STARTUP", + env_key="OPENPYPE_WORKFILE_TOOL_ON_START", default=None) # return if none is defined From e58534b2d6c82673f469f66138a4fe2a68eda049 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 28 Jul 2021 15:38:51 +0200 Subject: [PATCH 256/333] add model top group name validation --- .../plugins/publish/validate_model_name.py | 29 +++++++++++++++++++ .../defaults/project_settings/maya.json | 3 +- .../schemas/schema_maya_publish.json | 9 ++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_model_name.py b/openpype/hosts/maya/plugins/publish/validate_model_name.py index 64f06fb1fb..3757e13a9b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_model_name.py +++ b/openpype/hosts/maya/plugins/publish/validate_model_name.py @@ -3,6 +3,7 @@ from maya import cmds import pyblish.api import openpype.api +import avalon.api import openpype.hosts.maya.api.action from openpype.hosts.maya.api.shader_definition_editor import ( DEFINITION_FILENAME) @@ -51,6 +52,34 @@ class ValidateModelName(pyblish.api.InstancePlugin): cls.log.error("Instance has no nodes!") return True pass + + # validate top level group name + assemblies = cmds.ls(content_instance, assemblies=True, long=True) + if len(assemblies) != 1: + cls.log.error("Must have exactly one top group") + return assemblies or True + top_group = assemblies[0] + regex = cls.top_level_regex + r = re.compile(regex) + m = r.match(top_group) + if m is None: + cls.log.error("invalid name on: {}".format(top_group)) + cls.log.error("name doesn't match regex {}".format(regex)) + invalid.append(top_group) + else: + if "asset" in r.groupindex: + if m.group("asset") != avalon.api.Session["AVALON_ASSET"]: + cls.log.error("Invalid asset name in top level group.") + return top_group + if "subset" in r.groupindex: + if m.group("subset") != instance.data.get("subset"): + cls.log.error("Invalid subset name in top level group.") + return top_group + if "project" in r.groupindex: + if m.group("project") != avalon.api.Session["AVALON_PROJECT"]: + cls.log.error("Invalid project name in top level group.") + return top_group + descendants = cmds.listRelatives(content_instance, allDescendents=True, fullPath=True) or [] diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index b40ab40c61..1db6cdf9f1 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -170,7 +170,8 @@ "darwin": "", "linux": "" }, - "regex": "(.*)_(\\d)*_(?P.*)_(GEO)" + "regex": "(.*)_(\\d)*_(?P.*)_(GEO)", + "top_level_regex": ".*_GRP" }, "ValidateTransformNamingSuffix": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 10b80dddfd..89cd30aed0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -167,6 +167,15 @@ "type": "text", "key": "regex", "label": "Validation regex" + }, + { + "type": "label", + "label": "Regex for validating name of top level group name.
You can use named capturing groups:
(?P<asset>.*) for Asset name
(?P<subset>.*) for Subset
(?P<project>.*) for project

For example to check for asset in name so *_some_asset_name_GRP is valid, use:
.*?_(?P<asset>.*)_GEO" + }, + { + "type": "text", + "key": "top_level_regex", + "label": "Top level group name regex" } ] }, From 60defafff47266741e51ceaa31e5f83b0b6be984 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 28 Jul 2021 15:59:25 +0200 Subject: [PATCH 257/333] updated documentation --- website/docs/admin_hosts_maya.md | 19 ++++++++++++++++++ .../maya-admin_model_name_validator.png | Bin 19794 -> 34893 bytes 2 files changed, 19 insertions(+) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 81aa64f9d6..d38ab8d8ad 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -65,6 +65,25 @@ in either file or database `foo` and `bar`. Object named `SomeCube_0001_foo_GEO` will pass but `SomeCube_GEO` will not and `SomeCube_001_xxx_GEO` will not too. +##### Top level group name +There is a validation for top level group name too. You can specify whatever regex you'd like to use. Default will +pass everything with `_GRP` suffix. You can use *named capturing groups* to validate against specific data. If you +put `(?P.*)` it will try to match everything captured in that group against current asset name. Likewise you can +use it for **subset** and **project** - `(?P.*)` and `(?P.*)`. + +**Example** + +You are working on asset (shot) `0030_OGC_0190`. You have this regex in **Top level group name**: +```regexp +.*?_(?P.*)_GRP +``` + +When you publish your model with top group named like `foo_GRP` it will fail. But with `foo_0030_OGC_0190_GRP` it will pass. + +:::info About regex +All regexes used here are in Python variant. +::: + ### Custom Menu You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**. ![Custom menu definition](assets/maya-admin_scriptsmenu.png) diff --git a/website/docs/assets/maya-admin_model_name_validator.png b/website/docs/assets/maya-admin_model_name_validator.png index 39ec2b2d211a27ac7962dadaaae4c2af4241f797..d1b92c5fc3edbc7e3c1cdb0753804a4fa11dcfb8 100644 GIT binary patch literal 34893 zcmb@u1yo$?wlxSLL4rqc3j_)7!Citwa0-fG!L3N)5<-GYaCeu&T|;p90)o2~?odb< z=bU@{_WR%K(em#x7>r_XzWwQ1bIrL{1*s~_Vm~K&j)a7SE%!m{BNEb+Oe7@aYIIb@ zU!JJl!w|oaoj%G+Ae9c2?I1opH5XSBM?$KIz`QkjhWL!(@IluJ2??j;@dvrbzQ7a- zDPUDjN?gMgxWC}#@M=1h?Y8W#U zYrW8A%O)ajFU_&U=y_t)BeeA;J#EJld^xynMig||!}IxO|BOH1h#9OuxOzpA!7E?$esN`1r@Mg@Xl`(qb2X<8!vh&uWY8=(N<-)LG3Yc=A$+ z%dLkhCJ=F*go`sgKCnUVr;4~w-7kom`FKCZ1xq3bai8Ik==+Pm8sPY+0RVo){;kfG zz`Tbc#=%_%y7XajB5`v7t^=&)^0*1+RQi(z#{js3?% zB*@{?RMQXmQNUrBAMVxBVz;s9QKa!a`m2@AyVaV-jq{bIwQj+x`s)MiW)~QN-I52X zwddtZ_T@qV>3eCNoA#)#xx}f1U(=Uf1U~CNXk}r_#fi$E7WXG>fu6hKG0i-I$D~#l z!7-A$m8F!30qJ2cHu2;y5-4`XfJ|E5oaH zg={NjUsngrq`Tb~^wNhT@$=TNLRZIKXa|VJ(r|r#ku+LV1irfKIh+EBaW&k*TtW#{ zal0t*@DA>lCF?Kjccy7vqgX(Kyunm;l%n_f(~&=`T94>w-hHd716~`ts?GWLP%aoh z&FvrTQs-OD&#m=>5ni?1zt3G95Ywp5`&0W8u)2Cm?|^(@*-tR#1(>LN(tT9r&kbFyiY{@Kw(h!s-E}22LO8w8 zj5@-&C!3nBw_$OkmNd+BVC_h;;c9x55@7POF5eek^TE$+v(mh0MT%%t*|P*jOKX-^w*0F?XO{RxH4n1kB=W}(4i4c BI+%$ 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 literal 19794 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 Vy(H!b1HAwM002ovPDHLkV1k)@_e%f( From 490ec264b3c147562ba10ea39b46d70ca47c0029 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 29 Jul 2021 08:43:08 +0100 Subject: [PATCH 258/333] Normalize path returned from Workfiles. --- openpype/tools/workfiles/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index d567e26d74..6b56322140 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -693,7 +693,7 @@ class FilesWidget(QtWidgets.QWidget): ) return - file_path = os.path.join(self.root, work_file) + file_path = os.path.join(os.path.normpath(self.root), work_file) pipeline.emit("before.workfile.save", file_path) From 03a894ac9503c254eb7fb0c0c082a53c54a10b3f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 29 Jul 2021 09:24:35 +0100 Subject: [PATCH 259/333] Allow Multiple Notes to run on tasks. --- .../ftrack/event_handlers_user/action_multiple_notes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py index 8db65fe39b..666e7efaef 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py @@ -9,14 +9,15 @@ class MultipleNotes(BaseAction): #: Action label. label = 'Multiple Notes' #: Action description. - description = 'Add same note to multiple Asset Versions' + description = 'Add same note to multiple entities' icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg") def discover(self, session, entities, event): ''' Validation ''' + valid_entity_types = ['assetversion', 'task'] valid = True for entity in entities: - if entity.entity_type.lower() != 'assetversion': + if entity.entity_type.lower() not in valid_entity_types: valid = False break return valid @@ -58,7 +59,7 @@ class MultipleNotes(BaseAction): splitter = { 'type': 'label', - 'value': '{}'.format(200*"-") + 'value': '{}'.format(200 * "-") } items = [] From 02780730d62fe5d05037bd20ecc1cebb05a9b9e1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 29 Jul 2021 09:34:30 +0100 Subject: [PATCH 260/333] Check for multiple selection. --- .../ftrack/event_handlers_user/action_multiple_notes.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py index 666e7efaef..f5af044de0 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py @@ -14,12 +14,19 @@ class MultipleNotes(BaseAction): def discover(self, session, entities, event): ''' Validation ''' - valid_entity_types = ['assetversion', 'task'] valid = True + + # Check for multiple selection. + if len(entities) < 2: + valid = False + + # Check for valid entities. + valid_entity_types = ['assetversion', 'task'] for entity in entities: if entity.entity_type.lower() not in valid_entity_types: valid = False break + return valid def interface(self, session, entities, event): From 09fc70c30387cb14e6b96bcff6acc8206fac5501 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 29 Jul 2021 16:34:00 +0200 Subject: [PATCH 261/333] tweak doc headings --- website/docs/admin_hosts_maya.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index d38ab8d8ad..5e0aa15345 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -4,11 +4,11 @@ title: Maya sidebar_label: Maya --- -## Maya +## Publish Plugins -### Publish Plugins +### Render Settings Validator -#### Render Settings Validator (`ValidateRenderSettings`) +`ValidateRenderSettings` Render Settings Validator is here to make sure artists will submit renders we correct settings. Some of these settings are needed by OpenPype but some @@ -51,7 +51,10 @@ just one instance of this node type but if that is not so, validator will go thr instances and check the value there. Node type for **VRay** settings is `VRaySettingsNode`, for **Renderman** it is `rmanGlobals`, for **Redshift** it is `RedshiftOptions`. -#### Model Name Validator (`ValidateRenderSettings`) +### Model Name Validator + +`ValidateRenderSettings` + This validator can enforce specific names for model members. It will check them against **Validation Regex**. There is special group in that regex - **shader**. If present, it will take that part of the name as shader name and it will compare it with list of shaders defined either in file name specified in **Material File** or from @@ -65,7 +68,7 @@ in either file or database `foo` and `bar`. Object named `SomeCube_0001_foo_GEO` will pass but `SomeCube_GEO` will not and `SomeCube_001_xxx_GEO` will not too. -##### Top level group name +#### Top level group name There is a validation for top level group name too. You can specify whatever regex you'd like to use. Default will pass everything with `_GRP` suffix. You can use *named capturing groups* to validate against specific data. If you put `(?P.*)` it will try to match everything captured in that group against current asset name. Likewise you can @@ -84,7 +87,7 @@ When you publish your model with top group named like `foo_GRP` it will fail. Bu All regexes used here are in Python variant. ::: -### Custom Menu +## Custom Menu You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**. ![Custom menu definition](assets/maya-admin_scriptsmenu.png) From 12063d2b5ff38f7d3d900f8d41d8c839be80120d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 29 Jul 2021 19:16:40 +0200 Subject: [PATCH 262/333] added example of using template as object type in list --- .../example_infinite_hierarchy.json | 58 +++++++++++++++++++ .../schemas/system_schema/example_schema.json | 11 ++++ 2 files changed, 69 insertions(+) create mode 100644 openpype/settings/entities/schemas/system_schema/example_infinite_hierarchy.json diff --git a/openpype/settings/entities/schemas/system_schema/example_infinite_hierarchy.json b/openpype/settings/entities/schemas/system_schema/example_infinite_hierarchy.json new file mode 100644 index 0000000000..a2660e9bf2 --- /dev/null +++ b/openpype/settings/entities/schemas/system_schema/example_infinite_hierarchy.json @@ -0,0 +1,58 @@ +[ + { + "type": "dict-conditional", + "use_label_wrap": true, + "collapsible": true, + "key": "menu_items", + "label": "Menu items", + "enum_key": "type", + "enum_label": "Type", + "enum_children": [ + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "command", + "label": "Comand" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "key": "children", + "label": "Children", + "type": "list", + "object_type": { + "type": "template", + "name": "example_infinite_hierarchy" + } + } + ] + }, + { + "key": "separator", + "label": "Separator" + } + ] + } +] diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index c3287d7452..71a15ca721 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -57,6 +57,17 @@ } ] }, + { + "type": "list", + "use_label_wrap": true, + "collapsible": true, + "key": "infinite_hierarchy", + "label": "Infinite list template hierarchy", + "object_type": { + "type": "template", + "name": "example_infinite_hierarchy" + } + }, { "type": "dict", "key": "schema_template_exaples", From 18184a321bc04adcda5e5c5e5cdd0458e6ec7dc0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jul 2021 12:06:15 +0100 Subject: [PATCH 263/333] Increment workfile plugin --- .../publish/increment_workfile_version.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py diff --git a/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py b/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py new file mode 100644 index 0000000000..a96a8e3d5d --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/increment_workfile_version.py @@ -0,0 +1,22 @@ +import pyblish.api + +from avalon.tvpaint import workio +from openpype.api import version_up + + +class IncrementWorkfileVersion(pyblish.api.ContextPlugin): + """Increment current workfile version.""" + + order = pyblish.api.IntegratorOrder + 1 + label = "Increment Workfile Version" + optional = True + hosts = ["tvpaint"] + + def process(self, context): + + assert all(result["success"] for result in context.data["results"]), ( + "Publishing not succesfull so version is not increased.") + + path = context.data["currentFile"] + workio.save_file(version_up(path)) + self.log.info('Incrementing workfile version') From 7ec2cf735252c01b912049eec8a58c737651d04d Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 30 Jul 2021 12:12:56 +0100 Subject: [PATCH 264/333] Expose stop timer through rest api. --- openpype/modules/timers_manager/rest_api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/modules/timers_manager/rest_api.py b/openpype/modules/timers_manager/rest_api.py index 975c1a91f9..ac8d8b7b74 100644 --- a/openpype/modules/timers_manager/rest_api.py +++ b/openpype/modules/timers_manager/rest_api.py @@ -3,6 +3,7 @@ from openpype.api import Logger log = Logger().get_logger("Event processor") + class TimersManagerModuleRestApi: """ REST API endpoint used for calling from hosts when context change @@ -22,6 +23,11 @@ class TimersManagerModuleRestApi: self.prefix + "/start_timer", self.start_timer ) + self.server_manager.add_route( + "POST", + self.prefix + "/stop_timer", + self.stop_timer + ) async def start_timer(self, request): data = await request.json() @@ -38,3 +44,7 @@ class TimersManagerModuleRestApi: self.module.stop_timers() self.module.start_timer(project_name, asset_name, task_name, hierarchy) return Response(status=200) + + async def stop_timer(self, request): + self.module.stop_timers() + return Response(status=200) From 0df9744f29de5e20f7de1c2c3a72fc87f6968888 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jul 2021 16:08:54 +0200 Subject: [PATCH 265/333] publisher: editorial plugins fixes --- .../plugins/publish/collect_editorial_instances.py | 2 +- .../plugins/publish/collect_hierarchy.py | 12 ++++++------ .../plugins/publish/extract_trim_video_audio.py | 2 +- .../project_settings/standalonepublisher.json | 2 +- .../schema_project_standalonepublisher.json | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index 60a8cf48fc..3a9a7a3445 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -182,7 +182,7 @@ class CollectInstances(pyblish.api.InstancePlugin): }) for subset, properities in self.subsets.items(): version = properities.get("version") - if version and version == 0: + if version == 0: properities.pop("version") # adding Review-able instance diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py index ba2aed4bfc..acad98d784 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py @@ -37,7 +37,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin): # return if any if entity_type: - return {"entityType": entity_type, "entityName": value} + return {"entity_type": entity_type, "entity_name": value} def rename_with_hierarchy(self, instance): search_text = "" @@ -76,8 +76,8 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin): # add current selection context hierarchy from standalonepublisher for entity in reversed(visual_hierarchy): parents.append({ - "entityType": entity["data"]["entityType"], - "entityName": entity["name"] + "entity_type": entity["data"]["entityType"], + "entity_name": entity["name"] }) if self.shot_add_hierarchy: @@ -98,7 +98,7 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin): # in case SP context is set to the same folder if (_index == 0) and ("folder" in parent_key) \ - and (parents[-1]["entityName"] == parent_filled): + and (parents[-1]["entity_name"] == parent_filled): self.log.debug(f" skiping : {parent_filled}") continue @@ -280,9 +280,9 @@ class CollectHierarchyContext(pyblish.api.ContextPlugin): for parent in reversed(parents): next_dict = {} - parent_name = parent["entityName"] + parent_name = parent["entity_name"] next_dict[parent_name] = {} - next_dict[parent_name]["entity_type"] = parent["entityType"] + next_dict[parent_name]["entity_type"] = parent["entity_type"] next_dict[parent_name]["childs"] = actual actual = next_dict diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py index eb613fa951..059ac9603c 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_trim_video_audio.py @@ -60,7 +60,7 @@ class ExtractTrimVideoAudio(openpype.api.Extractor): ] args = [ - ffmpeg_path, + f"\"{ffmpeg_path}\"", "-ss", str(start / fps), "-i", f"\"{video_file_path}\"", "-t", str(dur / fps) diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 7d5cd4d8a1..50c1e34366 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -254,7 +254,7 @@ }, "shot_add_tasks": {} }, - "shot_add_tasks": { + "CollectInstances": { "custom_start_frame": 0, "timeline_frame_start": 900000, "timeline_frame_offset": 0, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 0af32c8287..37fcaac69f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -327,7 +327,7 @@ { "type": "dict", "collapsible": true, - "key": "shot_add_tasks", + "key": "CollectInstances", "label": "Collect Clip Instances", "is_group": true, "children": [ From ace014c777cf08223a11eeee3cc94435289a424d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Jul 2021 16:16:47 +0200 Subject: [PATCH 266/333] fix exceptions --- openpype/settings/entities/dict_conditional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 96065b670e..b61f667f6d 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -185,13 +185,13 @@ class DictConditionalEntity(ItemEntity): children_def_keys = [] for children_def in self.enum_children: if not isinstance(children_def, dict): - raise EntitySchemaError(( + raise EntitySchemaError(self, ( "Children definition under key 'enum_children' must" " be a dictionary." )) if "key" not in children_def: - raise EntitySchemaError(( + raise EntitySchemaError(self, ( "Children definition under key 'enum_children' miss" " 'key' definition." )) From 73a13a13cd6a9e2a1711e439e67c4cd9d4538eef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Jul 2021 16:18:35 +0200 Subject: [PATCH 267/333] added new enum attributes --- openpype/settings/entities/dict_conditional.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index b61f667f6d..b48c5a1cb0 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -144,6 +144,13 @@ class DictConditionalEntity(ItemEntity): self.enum_entity = None + # GUI attributes + self.enum_is_horizontal = self.schema_data.get( + "enum_is_horizontal", False + ) + # `enum_on_right` can be used only if + self.enum_on_right = self.schema_data.get("enum_on_right", False) + self.highlight_content = self.schema_data.get( "highlight_content", False ) From e4c050611d102771c4976db8347032b4cd33b897 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Jul 2021 16:20:55 +0200 Subject: [PATCH 268/333] modified widget to be able show combobox horizontally --- .../settings/settings/dict_conditional.py | 63 +++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index da2f53497e..31a4fa9fab 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -24,6 +24,7 @@ class DictConditionalWidget(BaseWidget): self.body_widget = None self.content_widget = None self.content_layout = None + self.enum_layout = None label = None if self.entity.is_dynamic_item: @@ -40,8 +41,36 @@ class DictConditionalWidget(BaseWidget): self._enum_key_by_wrapper_id = {} self._added_wrapper_ids = set() - self.content_layout.setColumnStretch(0, 0) - self.content_layout.setColumnStretch(1, 1) + enum_layout = QtWidgets.QGridLayout() + enum_layout.setContentsMargins(0, 0, 0, 0) + enum_layout.setColumnStretch(0, 0) + enum_layout.setColumnStretch(1, 1) + + all_children_layout = QtWidgets.QVBoxLayout() + all_children_layout.setContentsMargins(0, 0, 0, 0) + + if self.entity.enum_is_horizontal: + if self.entity.enum_on_right: + self.content_layout.addLayout(all_children_layout, 0, 0) + self.content_layout.addLayout(enum_layout, 0, 1) + # Stretch combobox to minimum and expand value + self.content_layout.setColumnStretch(0, 1) + self.content_layout.setColumnStretch(1, 0) + else: + self.content_layout.addLayout(enum_layout, 0, 0) + self.content_layout.addLayout(all_children_layout, 0, 1) + # Stretch combobox to minimum and expand value + self.content_layout.setColumnStretch(0, 0) + self.content_layout.setColumnStretch(1, 1) + + else: + # Expand content + self.content_layout.setColumnStretch(0, 1) + self.content_layout.addLayout(enum_layout, 0, 0) + self.content_layout.addLayout(all_children_layout, 1, 0) + + self.enum_layout = enum_layout + self.all_children_layout = all_children_layout # Add enum entity to layout mapping enum_entity = self.entity.enum_entity @@ -58,6 +87,8 @@ class DictConditionalWidget(BaseWidget): content_layout.setContentsMargins(0, 0, 0, 0) content_layout.setSpacing(5) + all_children_layout.addWidget(content_widget) + self._content_by_enum_value[enum_key] = { "widget": content_widget, "layout": content_layout @@ -80,9 +111,6 @@ class DictConditionalWidget(BaseWidget): for item_key, children in self.entity.children.items(): content_widget = self._content_by_enum_value[item_key]["widget"] - row = self.content_layout.rowCount() - self.content_layout.addWidget(content_widget, row, 0, 1, 2) - for child_obj in children: self.input_fields.append( self.create_ui_for_entity( @@ -191,12 +219,25 @@ class DictConditionalWidget(BaseWidget): else: map_id = widget.entity.id - content_widget = self.content_widget - content_layout = self.content_layout - if map_id != self.entity.enum_entity.id: - enum_value = self._enum_key_by_wrapper_id[map_id] - content_widget = self._content_by_enum_value[enum_value]["widget"] - content_layout = self._content_by_enum_value[enum_value]["layout"] + is_enum_item = map_id == self.entity.enum_entity.id + if is_enum_item: + content_widget = self.content_widget + content_layout = self.enum_layout + + if not label: + content_layout.addWidget(widget, 0, 0, 1, 2) + return + + label_widget = GridLabelWidget(label, widget) + label_widget.input_field = widget + widget.label_widget = label_widget + content_layout.addWidget(label_widget, 0, 0, 1, 1) + content_layout.addWidget(widget, 0, 1, 1, 1) + return + + enum_value = self._enum_key_by_wrapper_id[map_id] + content_widget = self._content_by_enum_value[enum_value]["widget"] + content_layout = self._content_by_enum_value[enum_value]["layout"] wrapper = self._parent_widget_by_entity_id[map_id] if wrapper is not content_widget: From 8ad04c84f62840c86634f8cd82cfd97f7b8927bd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Jul 2021 16:21:10 +0200 Subject: [PATCH 269/333] allow to not set label --- openpype/settings/entities/dict_conditional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index b48c5a1cb0..d275d8ac3d 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -293,7 +293,7 @@ class DictConditionalEntity(ItemEntity): "multiselection": False, "enum_items": enum_items, "key": enum_key, - "label": self.enum_label or enum_key + "label": self.enum_label } enum_entity = self.create_schema_object(enum_schema, self) From dc93c7a786bdf3808a49ff92865f69e8c11db23b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jul 2021 16:21:39 +0200 Subject: [PATCH 270/333] global: integrate name missing default template --- openpype/plugins/publish/integrate_new.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6d2a95f232..bc810e9125 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -303,6 +303,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): key_values = {"families": family, "tasks": task_name} profile = filter_profiles(self.template_name_profiles, key_values, logger=self.log) + + template_name = "publish" if profile: template_name = profile["template_name"] From 860bb00ed5b466d3ec75b9de00433de138049d80 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Jul 2021 16:22:19 +0200 Subject: [PATCH 271/333] added example of `enum_is_horizontal` usage --- .../schemas/system_schema/example_schema.json | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index c3287d7452..8ec97064a1 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -9,6 +9,31 @@ "label": "Color input", "type": "color" }, + { + "type": "dict-conditional", + "key": "overriden_value", + "label": "Overriden value", + "enum_key": "overriden", + "enum_is_horizontal": true, + "enum_children": [ + { + "key": "overriden", + "label": "Override value", + "children": [ + { + "type": "number", + "key": "value", + "label": "value" + } + ] + }, + { + "key": "inherit", + "label": "Inherit value", + "children": [] + } + ] + }, { "type": "dict-conditional", "use_label_wrap": true, From b0d0e41c98e78dffbce2c87fc7cf3a2905904817 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jul 2021 16:23:12 +0200 Subject: [PATCH 272/333] removing blank line space --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bc810e9125..3504206fe1 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -303,7 +303,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): key_values = {"families": family, "tasks": task_name} profile = filter_profiles(self.template_name_profiles, key_values, logger=self.log) - + template_name = "publish" if profile: template_name = profile["template_name"] From f34f45c3fbf8fb2568ed69483dd0719c9e9f9520 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 30 Jul 2021 16:25:18 +0200 Subject: [PATCH 273/333] added enum_is_horizontal to readme --- openpype/settings/entities/schemas/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 079d16c506..399c4ac1d9 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -204,6 +204,8 @@ - it is possible to add darker background with `"highlight_content"` (Default: `False`) - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color - output is dictionary `{the "key": children values}` +- for UI porposes was added `enum_is_horizontal` which will make combobox appear next to children inputs instead of on top of them (Default: `False`) + - this has extended ability of `enum_on_right` which will move combobox to right side next to children widgets (Default: `False`) ``` # Example { From 74f57039e4d390f27c1450f390b3a1c1ffd34b7a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jul 2021 16:37:05 +0200 Subject: [PATCH 274/333] global: better label --- openpype/plugins/publish/validate_editorial_asset_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index ccea42dc37..f13e3b4f38 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -11,7 +11,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): """ order = pyblish.api.ValidatorOrder - label = "Validate Asset Name" + label = "Validate Editorial Asset Name" def process(self, context): From e8f773efa188f90b26abc47469fe18760431c9a7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 30 Jul 2021 16:37:40 +0200 Subject: [PATCH 275/333] settings: global validators with options --- .../defaults/project_settings/global.json | 8 ++++ .../schemas/schema_global_publish.json | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 636acc0d17..c14486f384 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -1,5 +1,13 @@ { "publish": { + "ValidateEditorialAssetName": { + "enabled": true, + "optional": false + }, + "ValidateVersion": { + "enabled": true, + "optional": false + }, "IntegrateHeroVersion": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 4715db4888..a1cbc8639f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -4,6 +4,46 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "ValidateEditorialAssetName", + "label": "Validate Editorial Asset Name", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "ValidateVersion", + "label": "Validate Version", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + } + ] + }, { "type": "dict", "collapsible": true, From 56dfb1b12606e39f568d64d202b53f043beda2ee Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 31 Jul 2021 03:41:18 +0000 Subject: [PATCH 276/333] [Automated] Bump version --- CHANGELOG.md | 27 +++++++++++++-------------- openpype/version.py | 2 +- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbd5ccd412..8a41ccb4d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,17 @@ # Changelog -## [3.3.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) **🚀 Enhancements** +- Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886) +- Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882) +- Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869) +- Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868) +- Local actions with process identifier [\#1867](https://github.com/pypeclub/OpenPype/pull/1867) +- Workfile tool start at host launch support [\#1865](https://github.com/pypeclub/OpenPype/pull/1865) - Anatomy schema validation [\#1864](https://github.com/pypeclub/OpenPype/pull/1864) - Ftrack prepare project structure [\#1861](https://github.com/pypeclub/OpenPype/pull/1861) - Independent general environments [\#1853](https://github.com/pypeclub/OpenPype/pull/1853) @@ -20,23 +26,23 @@ **🐛 Bug fixes** +- Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880) +- Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862) - imageio: fix grouping [\#1856](https://github.com/pypeclub/OpenPype/pull/1856) - publisher: missing version in subset prop [\#1849](https://github.com/pypeclub/OpenPype/pull/1849) - Ftrack type error fix in sync to avalon event handler [\#1845](https://github.com/pypeclub/OpenPype/pull/1845) - Nuke: updating effects subset fail [\#1841](https://github.com/pypeclub/OpenPype/pull/1841) +- Fix - Standalone Publish better handling of loading multiple versions… [\#1837](https://github.com/pypeclub/OpenPype/pull/1837) - nuke: write render node skipped with crop [\#1836](https://github.com/pypeclub/OpenPype/pull/1836) - Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) - Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Failsafe for cross project containers. [\#1806](https://github.com/pypeclub/OpenPype/pull/1806) -- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) -- Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) +- Settings error dialog on show [\#1798](https://github.com/pypeclub/OpenPype/pull/1798) **Merged pull requests:** -- Ftrack push attributes action adds traceback to job [\#1842](https://github.com/pypeclub/OpenPype/pull/1842) - Add support for pyenv-win on windows [\#1822](https://github.com/pypeclub/OpenPype/pull/1822) - PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811) -- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808) ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) @@ -61,6 +67,7 @@ **🐛 Bug fixes** +- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) - Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) - Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) - Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) @@ -82,9 +89,9 @@ **Merged pull requests:** +- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808) - Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) - Bc/fix/docs [\#1771](https://github.com/pypeclub/OpenPype/pull/1771) -- Expose write attributes to config [\#1770](https://github.com/pypeclub/OpenPype/pull/1770) - TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) @@ -100,18 +107,10 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) -**🐛 Bug fixes** - -- Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) - ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2) -**🐛 Bug fixes** - -- Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713) - ## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.1.0-nightly.4...3.1.0) diff --git a/openpype/version.py b/openpype/version.py index d7efcf6bd5..ee121051ea 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.5" +__version__ = "3.3.0-nightly.6" From 0963f3b776ac703c6d3c2890cf53bd416b115ed8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 09:07:00 +0200 Subject: [PATCH 277/333] fixed python detection --- tools/create_env.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index 2ab6abe76e..e2ec401bb3 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -62,9 +62,12 @@ function Test-Python() { Write-Host "Detecting host Python ... " -NoNewline $python = "python" if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { - $python = & pyenv which python + $pyenv_python = & pyenv which python + if (Test-Path -PathType Leaf -Path "$($pyenv_python)") { + $python = $pyenv_python + } } - if (-not (Get-Command "python3" -ErrorAction SilentlyContinue)) { + if (-not (Get-Command $python -ErrorAction SilentlyContinue)) { Write-Host "!!! Python not detected" -ForegroundColor red Set-Location -Path $current_dir Exit-WithCode 1 From e5c8814797f464e7863d94c970305c42a200bebe Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 10:50:50 +0200 Subject: [PATCH 278/333] removed unused function --- tools/build.ps1 | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index e1962ee933..10da3d0b83 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -80,17 +80,6 @@ function Show-PSWarning() { } } -function Install-Poetry() { - Write-Host ">>> " -NoNewline -ForegroundColor Green - Write-Host "Installing Poetry ... " - $python = "python" - if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { - $python = & pyenv which python - } - $env:POETRY_HOME="$openpype_root\.poetry" - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | & $($python) - -} - $art = @" . . .. . .. From 7a7d44e628f212293ef0a4d71ee8885420944d7d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 16:15:09 +0200 Subject: [PATCH 279/333] better error handling --- openpype/hosts/maya/api/__init__.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 027fa871e8..7af22e2ca8 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -30,19 +30,25 @@ def install(): project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} - if mapping.get("source-path") and project_settings["maya"]["maya-dirmap"]["enabled"] is True: + mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"] + if mapping.get("source-path") and mapping_enabled is True: log.info("Processing directory mapping ...") cmds.dirmap(en=True) for k, sp in enumerate(mapping["source-path"]): try: print("{} -> {}".format(sp, mapping["destination-path"][k])) - cmds.dirmap(m=[sp, mapping["destination-path"][k]]) - cmds.dirmap(m=[mapping["destination-path"][k], sp]) + cmds.dirmap(m=(sp, mapping["destination-path"][k])) + cmds.dirmap(m=(mapping["destination-path"][k], sp)) except IndexError: # missing corresponding destination path log.error(("invalid dirmap mapping, missing corresponding" " destination directory.")) break + except RuntimeError: + log.error("invalid path {} -> {}, mapping not registered".format( + sp, mapping["destination-path"][k] + )) + continue pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) From 0a9c335f9078f77c39c1efbf810ae0554b701797 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 16:32:10 +0200 Subject: [PATCH 280/333] =?UTF-8?q?=E2=86=A9=EF=B8=8F=20backward=20compati?= =?UTF-8?q?bility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/api/__init__.py | 56 ++++++++++++++++++----------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 7af22e2ca8..9219da407f 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -29,26 +29,8 @@ def install(): from openpype.settings import get_project_settings project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) - mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} - mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"] - if mapping.get("source-path") and mapping_enabled is True: - log.info("Processing directory mapping ...") - cmds.dirmap(en=True) - for k, sp in enumerate(mapping["source-path"]): - try: - print("{} -> {}".format(sp, mapping["destination-path"][k])) - cmds.dirmap(m=(sp, mapping["destination-path"][k])) - cmds.dirmap(m=(mapping["destination-path"][k], sp)) - except IndexError: - # missing corresponding destination path - log.error(("invalid dirmap mapping, missing corresponding" - " destination directory.")) - break - except RuntimeError: - log.error("invalid path {} -> {}, mapping not registered".format( - sp, mapping["destination-path"][k] - )) - continue + # process path mapping + process_dirmap(project_settings) pyblish.register_plugin_path(PUBLISH_PATH) avalon.register_plugin_path(avalon.Loader, LOAD_PATH) @@ -77,6 +59,40 @@ def install(): avalon.data["familiesStateToggled"] = ["imagesequence"] +def process_dirmap(project_settings): + # type: (dict) -> None + """Go through all paths in Settings and set them using `dirmap`. + + Args: + project_settings (dict): Settings for current project. + + """ + if not project_settings["maya"].get("maya-dirmap"): + return + mapping = project_settings["maya"]["maya-dirmap"]["paths"] or {} + mapping_enabled = project_settings["maya"]["maya-dirmap"]["enabled"] + if not mapping or not mapping_enabled: + return + if mapping.get("source-path") and mapping_enabled is True: + log.info("Processing directory mapping ...") + cmds.dirmap(en=True) + for k, sp in enumerate(mapping["source-path"]): + try: + print("{} -> {}".format(sp, mapping["destination-path"][k])) + cmds.dirmap(m=(sp, mapping["destination-path"][k])) + cmds.dirmap(m=(mapping["destination-path"][k], sp)) + except IndexError: + # missing corresponding destination path + log.error(("invalid dirmap mapping, missing corresponding" + " destination directory.")) + break + except RuntimeError: + log.error("invalid path {} -> {}, mapping not registered".format( + sp, mapping["destination-path"][k] + )) + continue + + def uninstall(): pyblish.deregister_plugin_path(PUBLISH_PATH) avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH) From 066765427e622fb4ef7988a6bc0828e5cda4a5ce Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 16:54:06 +0200 Subject: [PATCH 281/333] =?UTF-8?q?add=20documentation=20=F0=9F=A7=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- website/docs/admin_hosts_maya.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 5e0aa15345..7a928483bb 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -94,4 +94,9 @@ You can add your custom tools menu into Maya by extending definitions in **Maya :::note Work in progress This is still work in progress. Menu definition will be handled more friendly with widgets and not raw json. -::: \ No newline at end of file +::: + +## Multiplatform path mapping +You can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between +list of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping** +![Dirmap settings](assets/maya-admin_dirmap_settings.png) From af6282953f7952c9c682364cee11e5122c193093 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Aug 2021 16:55:04 +0200 Subject: [PATCH 282/333] =?UTF-8?q?add=20image=20=F0=9F=96=BC=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/assets/maya-admin_dirmap_settings.png | Bin 0 -> 15234 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/maya-admin_dirmap_settings.png diff --git a/website/docs/assets/maya-admin_dirmap_settings.png b/website/docs/assets/maya-admin_dirmap_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..9d5780dfc815a49832bc33ea70f444adf38b3c20 GIT binary patch literal 15234 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 H#(w_?-F&;- literal 0 HcmV?d00001 From af45f466d5f81e4d7daca097ff06b9df0008b92a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 3 Aug 2021 17:13:35 +0200 Subject: [PATCH 283/333] remove whitespace --- openpype/hosts/maya/plugins/publish/extract_look.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index c823602dc4..f09d50d714 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -296,7 +296,7 @@ class ExtractLook(openpype.api.Extractor): remap[color_space_attr] = color_space attr = resource["attribute"] remap[attr] = destinations[source] - + self.log.info("Finished remapping destinations ...") # Extract in correct render layer From 0e2bf5f522b4668cbd0dc43cc54efb3c97737696 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 3 Aug 2021 17:49:21 +0200 Subject: [PATCH 284/333] compute environments after merge envs --- openpype/lib/applications.py | 8 ++++++-- start.py | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index ada194f15f..9f5a092afc 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1138,7 +1138,8 @@ def prepare_host_environments(data, implementation_envs=True): # Merge dictionaries env_values = _merge_env(tool_env, env_values) - loaded_env = _merge_env(acre.compute(env_values), data["env"]) + merged_env = _merge_env(computed_env, data["env"]) + loaded_env = acre.compute(merged_env, cleanup=False) final_env = None # Add host specific environments @@ -1189,7 +1190,10 @@ def apply_project_environments_value(project_name, env, project_settings=None): env_value = project_settings["global"]["project_environments"] if env_value: - env.update(_merge_env(acre.parse(env_value), env)) + env.update(acre.compute( + _merge_env(acre.parse(env_value), env), + cleanup=False + )) return env diff --git a/start.py b/start.py index 419a956835..6473a926d0 100644 --- a/start.py +++ b/start.py @@ -221,10 +221,14 @@ def set_openpype_global_environments() -> None: all_env = get_environments() general_env = all_env["global"] - env = acre.merge( + merged_env = acre.merge( acre.parse(general_env), dict(os.environ) ) + env = acre.compute( + merged_env, + cleanup=False + ) os.environ.clear() os.environ.update(env) From 6c0283f9ab9e70daf3fda03f2359b4dec11bf090 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 3 Aug 2021 17:54:56 +0200 Subject: [PATCH 285/333] fix variable --- openpype/lib/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 9f5a092afc..fe964d3bab 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1138,7 +1138,7 @@ def prepare_host_environments(data, implementation_envs=True): # Merge dictionaries env_values = _merge_env(tool_env, env_values) - merged_env = _merge_env(computed_env, data["env"]) + merged_env = _merge_env(env_values, data["env"]) loaded_env = acre.compute(merged_env, cleanup=False) final_env = None From 058089429fb5d95a22d55c10bf5b34ffb2175279 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 4 Aug 2021 03:42:17 +0000 Subject: [PATCH 286/333] [Automated] Bump version --- CHANGELOG.md | 18 ++++++++++-------- openpype/version.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a41ccb4d6..5e3f2150c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,15 @@ # Changelog -## [3.3.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) **🚀 Enhancements** +- Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892) +- Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891) - Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886) +- TVPaint: Increment workfile [\#1885](https://github.com/pypeclub/OpenPype/pull/1885) - Allow Multiple Notes to run on tasks. [\#1882](https://github.com/pypeclub/OpenPype/pull/1882) - Prepare for pyside2 [\#1869](https://github.com/pypeclub/OpenPype/pull/1869) - Filter hosts in settings host-enum [\#1868](https://github.com/pypeclub/OpenPype/pull/1868) @@ -23,24 +26,28 @@ - Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823) - Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) - Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) +- Maya: Shader name validation [\#1762](https://github.com/pypeclub/OpenPype/pull/1762) **🐛 Bug fixes** +- Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893) +- global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890) +- publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889) - Normalize path returned from Workfiles. [\#1880](https://github.com/pypeclub/OpenPype/pull/1880) - Workfiles tool event arguments fix [\#1862](https://github.com/pypeclub/OpenPype/pull/1862) - imageio: fix grouping [\#1856](https://github.com/pypeclub/OpenPype/pull/1856) - publisher: missing version in subset prop [\#1849](https://github.com/pypeclub/OpenPype/pull/1849) - Ftrack type error fix in sync to avalon event handler [\#1845](https://github.com/pypeclub/OpenPype/pull/1845) - Nuke: updating effects subset fail [\#1841](https://github.com/pypeclub/OpenPype/pull/1841) -- Fix - Standalone Publish better handling of loading multiple versions… [\#1837](https://github.com/pypeclub/OpenPype/pull/1837) - nuke: write render node skipped with crop [\#1836](https://github.com/pypeclub/OpenPype/pull/1836) - Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) - Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Failsafe for cross project containers. [\#1806](https://github.com/pypeclub/OpenPype/pull/1806) -- Settings error dialog on show [\#1798](https://github.com/pypeclub/OpenPype/pull/1798) +- Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) **Merged pull requests:** +- Maya: add support for `RedshiftNormalMap` node, fix `tx` linear space 🚀 [\#1863](https://github.com/pypeclub/OpenPype/pull/1863) - Add support for pyenv-win on windows [\#1822](https://github.com/pypeclub/OpenPype/pull/1822) - PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811) @@ -62,8 +69,6 @@ - Deadline: Nuke submission additional attributes [\#1756](https://github.com/pypeclub/OpenPype/pull/1756) - Settings schema without prefill [\#1753](https://github.com/pypeclub/OpenPype/pull/1753) - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) -- Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) -- PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) **🐛 Bug fixes** @@ -71,7 +76,6 @@ - Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) - Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) - Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) -- Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) - FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775) - Fix - single file files are str only, cast it to list to count properly [\#1772](https://github.com/pypeclub/OpenPype/pull/1772) - Environments in app executable for MacOS [\#1768](https://github.com/pypeclub/OpenPype/pull/1768) @@ -84,8 +88,6 @@ - Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) - Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741) - StandalonePublisher: failing collector for editorial [\#1738](https://github.com/pypeclub/OpenPype/pull/1738) -- Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) -- TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index ee121051ea..473be3bafc 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.6" +__version__ = "3.3.0-nightly.7" From b19b38a925e80e2d9d4fdf2bdb63ee52e7e02a15 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Aug 2021 13:55:27 +0200 Subject: [PATCH 287/333] updated acre commit --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index aad1898983..e011b781c9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ develop = false type = "git" url = "https://github.com/pypeclub/acre.git" reference = "master" -resolved_reference = "5a812c6dcfd3aada87adb49be98c548c894d6566" +resolved_reference = "55a7c331e6dc5f81639af50ca4a8cc9d73e9273d" [[package]] name = "aiohttp" From fe4a0ea2a51f4b224d2527f5af84bff6431e78e8 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 4 Aug 2021 15:08:52 +0100 Subject: [PATCH 288/333] Try formatting paths with current environment. --- openpype/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/__init__.py b/openpype/__init__.py index a86d2bc2be..e7462e14e9 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -98,6 +98,11 @@ def install(): .get(platform_name) ) or [] for path in project_plugins: + try: + path = str(path.format(**os.environ)) + except KeyError: + pass + if not path or not os.path.exists(path): continue From 550b8f38da42a767ee35f4d54e9987257f9e8241 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Aug 2021 17:40:09 +0200 Subject: [PATCH 289/333] added action which helps to identify if actions are shown on private project where ftrack event server does not have access --- .../action_private_project_detection.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 openpype/modules/ftrack/event_handlers_server/action_private_project_detection.py diff --git a/openpype/modules/ftrack/event_handlers_server/action_private_project_detection.py b/openpype/modules/ftrack/event_handlers_server/action_private_project_detection.py new file mode 100644 index 0000000000..5213e10ba3 --- /dev/null +++ b/openpype/modules/ftrack/event_handlers_server/action_private_project_detection.py @@ -0,0 +1,61 @@ +from openpype.modules.ftrack.lib import ServerAction + + +class PrivateProjectDetectionAction(ServerAction): + """Action helps to identify if does not have access to project.""" + + identifier = "server.missing.perm.private.project" + label = "Missing permissions" + description = ( + "Main ftrack event server does not have access to this project." + ) + + def _discover(self, event): + """Show action only if there is a selection in event data.""" + entities = self._translate_event(event) + if entities: + return None + + selection = event["data"].get("selection") + if not selection: + return None + + return { + "items": [{ + "label": self.label, + "variant": self.variant, + "description": self.description, + "actionIdentifier": self.discover_identifier, + "icon": self.icon, + }] + } + + def _launch(self, event): + # Ignore if there are values in event data + # - somebody clicked on submit button + values = event["data"].get("values") + if values: + return None + + title = "# Private project (missing permissions) #" + msg = ( + "User ({}) or API Key used on Ftrack event server" + " does not have permissions to access this private project." + ).format(self.session.api_user) + return { + "type": "form", + "title": "Missing permissions", + "items": [ + {"type": "label", "value": title}, + {"type": "label", "value": msg}, + # Add hidden to be able detect if was clicked on submit + {"type": "hidden", "value": "1", "name": "hidden"} + ], + "submit_button_label": "Got it" + } + + +def register(session): + '''Register plugin. Called when used as an plugin.''' + + PrivateProjectDetectionAction(session).register() From 83a2c0ff0481b0a2bc143a96dba471cbda6d63c7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 4 Aug 2021 18:06:39 +0200 Subject: [PATCH 290/333] merged where i run actions into one --- .../action_where_run_ask.py | 105 ++++++++++++++---- .../action_where_run_show.py | 86 -------------- 2 files changed, 85 insertions(+), 106 deletions(-) delete mode 100644 openpype/modules/ftrack/event_handlers_user/action_where_run_show.py diff --git a/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py b/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py index 6950d45ecd..2c427cfff7 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py +++ b/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py @@ -1,33 +1,98 @@ +import platform +import socket +import getpass + from openpype.modules.ftrack.lib import BaseAction, statics_icon -class ActionAskWhereIRun(BaseAction): - """ Sometimes user forget where pipeline with his credentials is running. - - this action triggers `ActionShowWhereIRun` - """ - ignore_me = True - identifier = 'ask.where.i.run' - label = 'Ask where I run' - description = 'Triggers PC info where user have running OpenPype' - icon = statics_icon("ftrack", "action_icons", "ActionAskWhereIRun.svg") +class ActionWhereIRun(BaseAction): + """Show where same user has running OpenPype instances.""" - def discover(self, session, entities, event): - """ Hide by default - Should be enabled only if you want to run. - - best practise is to create another action that triggers this one - """ + identifier = "ask.where.i.run" + show_identifier = "show.where.i.run" + label = "OpenPype Admin" + variant = "- Where I run" + description = "Show PC info where user have running OpenPype" - return True + def _discover(self, _event): + return { + "items": [{ + "label": self.label, + "variant": self.variant, + "description": self.description, + "actionIdentifier": self.discover_identifier, + "icon": self.icon, + }] + } - def launch(self, session, entities, event): - more_data = {"event_hub_id": session.event_hub.id} - self.trigger_action( - "show.where.i.run", event, additional_event_data=more_data + def _launch(self, event): + self.trigger_action(self.show_identifier, event) + + def register(self): + # Register default action callbacks + super(ActionWhereIRun, self).register() + + # Add show identifier + show_subscription = ( + "topic=ftrack.action.launch" + " and data.actionIdentifier={}" + " and source.user.username={}" + ).format( + self.show_identifier, + self.session.api_user + ) + self.session.event_hub.subscribe( + show_subscription, + self._show_info ) - return True + def _show_info(self, event): + title = "Where Do I Run?" + msgs = {} + all_keys = ["Hostname", "IP", "Username", "System name", "PC name"] + try: + host_name = socket.gethostname() + msgs["Hostname"] = host_name + host_ip = socket.gethostbyname(host_name) + msgs["IP"] = host_ip + except Exception: + pass + + try: + system_name, pc_name, *_ = platform.uname() + msgs["System name"] = system_name + msgs["PC name"] = pc_name + except Exception: + pass + + try: + msgs["Username"] = getpass.getuser() + except Exception: + pass + + for key in all_keys: + if not msgs.get(key): + msgs[key] = "-Undefined-" + + items = [] + first = True + separator = {"type": "label", "value": "---"} + for key, value in msgs.items(): + if first: + first = False + else: + items.append(separator) + self.log.debug("{}: {}".format(key, value)) + + subtitle = {"type": "label", "value": "

{}

".format(key)} + items.append(subtitle) + message = {"type": "label", "value": "

{}

".format(value)} + items.append(message) + + self.show_interface(items, title, event=event) def register(session): '''Register plugin. Called when used as an plugin.''' - ActionAskWhereIRun(session).register() + ActionWhereIRun(session).register() diff --git a/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py b/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py deleted file mode 100644 index b8b49e86cb..0000000000 --- a/openpype/modules/ftrack/event_handlers_user/action_where_run_show.py +++ /dev/null @@ -1,86 +0,0 @@ -import platform -import socket -import getpass -from openpype.modules.ftrack.lib import BaseAction - - -class ActionShowWhereIRun(BaseAction): - """ Sometimes user forget where pipeline with his credentials is running. - - this action shows on which PC, Username and IP is running - - requirement action MUST be registered where we want to locate the PC: - - - can't be used retrospectively... - """ - #: Action identifier. - identifier = 'show.where.i.run' - #: Action label. - label = 'Show where I run' - #: Action description. - description = 'Shows PC info where user have running OpenPype' - - def discover(self, session, entities, event): - """ Hide by default - Should be enabled only if you want to run. - - best practise is to create another action that triggers this one - """ - - return False - - @property - def launch_identifier(self): - return self.identifier - - def launch(self, session, entities, event): - # Don't show info when was launch from this session - if session.event_hub.id == event.get("data", {}).get("event_hub_id"): - return True - - title = "Where Do I Run?" - msgs = {} - all_keys = ["Hostname", "IP", "Username", "System name", "PC name"] - try: - host_name = socket.gethostname() - msgs["Hostname"] = host_name - host_ip = socket.gethostbyname(host_name) - msgs["IP"] = host_ip - except Exception: - pass - - try: - system_name, pc_name, *_ = platform.uname() - msgs["System name"] = system_name - msgs["PC name"] = pc_name - except Exception: - pass - - try: - msgs["Username"] = getpass.getuser() - except Exception: - pass - - for key in all_keys: - if not msgs.get(key): - msgs[key] = "-Undefined-" - - items = [] - first = True - splitter = {'type': 'label', 'value': '---'} - for key, value in msgs.items(): - if first: - first = False - else: - items.append(splitter) - self.log.debug("{}: {}".format(key, value)) - - subtitle = {'type': 'label', 'value': '

{}

'.format(key)} - items.append(subtitle) - message = {'type': 'label', 'value': '

{}

'.format(value)} - items.append(message) - - self.show_interface(items, title, event=event) - - return True - - -def register(session): - '''Register plugin. Called when used as an plugin.''' - - ActionShowWhereIRun(session).register() From cb7d8704e60580d949225c5f3e0abea1067487e9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Aug 2021 20:21:08 +0200 Subject: [PATCH 291/333] Moved Deadline settings from Global to Deadline plugin --- .../defaults/project_settings/deadline.json | 24 +++++ .../defaults/project_settings/global.json | 32 ++----- .../schema_project_deadline.json | 95 +++++++++++++++++++ .../schemas/schema_global_publish.json | 95 ------------------- 4 files changed, 129 insertions(+), 117 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 2dba20d63c..0f2da9f5b0 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -11,6 +11,30 @@ "deadline" ] }, + "ProcessSubmittedJobOnFarm": { + "enabled": true, + "deadline_department": "", + "deadline_pool": "", + "deadline_group": "", + "deadline_chunk_size": 1, + "deadline_priority": 50, + "publishing_script": "", + "skip_integration_repre_list": [], + "aov_filter": { + "maya": [ + ".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*" + ], + "nuke": [ + ".*" + ], + "aftereffects": [ + ".*" + ], + "celaction": [ + ".*" + ] + } + }, "MayaSubmitDeadline": { "enabled": true, "optional": false, diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index c14486f384..aab8c2196c 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -173,28 +173,6 @@ } ] }, - "ProcessSubmittedJobOnFarm": { - "enabled": true, - "deadline_department": "", - "deadline_pool": "", - "deadline_group": "", - "deadline_chunk_size": 1, - "deadline_priority": 50, - "publishing_script": "", - "skip_integration_repre_list": [], - "aov_filter": { - "maya": [ - ".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*" - ], - "nuke": [], - "aftereffects": [ - ".*" - ], - "celaction": [ - ".*" - ] - } - }, "CleanUp": { "paterns": [], "remove_temp_renders": false @@ -257,6 +235,16 @@ ], "tasks": [], "template": "{family}{Task}" + }, + { + "families": [ + "renderLocal" + ], + "hosts": [ + "aftereffects" + ], + "tasks": [], + "template": "render{Task}{Variant}" } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 27eeaef559..8e6a4b10e4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -52,6 +52,101 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ProcessSubmittedJobOnFarm", + "label": "ProcessSubmittedJobOnFarm", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "deadline_department", + "label": "Deadline department" + }, + { + "type": "text", + "key": "deadline_pool", + "label": "Deadline Pool" + }, + { + "type": "text", + "key": "deadline_group", + "label": "Deadline Group" + }, + { + "type": "number", + "key": "deadline_chunk_size", + "label": "Deadline Chunk Size" + }, + { + "type": "number", + "key": "deadline_priority", + "label": "Deadline Priotity" + }, + { + "type": "splitter" + }, + { + "type": "text", + "key": "publishing_script", + "label": "Publishing script path" + }, + { + "type": "list", + "key": "skip_integration_repre_list", + "label": "Skip integration of representation with ext", + "object_type": { + "type": "text" + } + }, + { + "type": "dict", + "key": "aov_filter", + "label": "Reviewable subsets filter", + "children": [ + { + "type": "list", + "key": "maya", + "label": "Maya", + "object_type": { + "type": "text" + } + }, + { + "type": "list", + "key": "nuke", + "label": "Nuke", + "object_type": { + "type": "text" + } + }, + { + "type": "list", + "key": "aftereffects", + "label": "After Effects", + "object_type": { + "type": "text" + } + }, + { + "type": "list", + "key": "celaction", + "label": "Celaction", + "object_type": { + "type": "text" + } + } + ] + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index a1cbc8639f..d265988534 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -556,101 +556,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "ProcessSubmittedJobOnFarm", - "label": "ProcessSubmittedJobOnFarm", - "checkbox_key": "enabled", - "is_group": true, - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "text", - "key": "deadline_department", - "label": "Deadline department" - }, - { - "type": "text", - "key": "deadline_pool", - "label": "Deadline Pool" - }, - { - "type": "text", - "key": "deadline_group", - "label": "Deadline Group" - }, - { - "type": "number", - "key": "deadline_chunk_size", - "label": "Deadline Chunk Size" - }, - { - "type": "number", - "key": "deadline_priority", - "label": "Deadline Priotity" - }, - { - "type": "splitter" - }, - { - "type": "text", - "key": "publishing_script", - "label": "Publishing script path" - }, - { - "type": "list", - "key": "skip_integration_repre_list", - "label": "Skip integration of representation with ext", - "object_type": { - "type": "text" - } - }, - { - "type": "dict", - "key": "aov_filter", - "label": "Reviewable subsets filter", - "children": [ - { - "type": "list", - "key": "maya", - "label": "Maya", - "object_type": { - "type": "text" - } - }, - { - "type": "list", - "key": "nuke", - "label": "Nuke", - "object_type": { - "type": "text" - } - }, - { - "type": "list", - "key": "aftereffects", - "label": "After Effects", - "object_type": { - "type": "text" - } - }, - { - "type": "list", - "key": "celaction", - "label": "Celaction", - "object_type": { - "type": "text" - } - } - ] - } - ] - }, { "type": "dict", "collapsible": true, From 9585240a2b3c1fe67fe372be59332a522db92e63 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Aug 2021 20:21:48 +0200 Subject: [PATCH 292/333] #122 - AE local - added ftrack family --- .../settings/defaults/project_settings/ftrack.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index dae5a591e9..9fa78ac588 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -298,6 +298,17 @@ "add_ftrack_family": true } ] + }, + { + "hosts": [ + "aftereffects" + ], + "families": [ + "render" + ], + "tasks": [], + "add_ftrack_family": true, + "advanced_filtering": [] } ] }, From 5ee187424d6c6e67e2ed5308cdf9ce22733cd67a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Aug 2021 20:22:24 +0200 Subject: [PATCH 293/333] #122 - AE local - added local creator --- .../plugins/create/create_local_render.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 openpype/hosts/aftereffects/plugins/create/create_local_render.py diff --git a/openpype/hosts/aftereffects/plugins/create/create_local_render.py b/openpype/hosts/aftereffects/plugins/create/create_local_render.py new file mode 100644 index 0000000000..9cc06eb698 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/create/create_local_render.py @@ -0,0 +1,17 @@ +from openpype.hosts.aftereffects.plugins.create import create_render + +import logging + +log = logging.getLogger(__name__) + + +class CreateLocalRender(create_render.CreateRender): + """ Creator to render locally. + + Created only after default render on farm. So family 'render.local' is + used for backward compatibility. + """ + + name = "renderDefault" + label = "Render Locally" + family = "renderLocal" From dfe538ae2630e68967624791e666e1715e083c5f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 4 Aug 2021 20:24:01 +0200 Subject: [PATCH 294/333] #122 - AE local - added local render functionality Added local render extract plugin Updated validator to trigger on local render too Updated current Deadline collector for local render --- .../plugins/publish/collect_render.py | 53 +++++++++++--- .../plugins/publish/extract_local_render.py | 69 +++++++++++++++++++ .../publish/validate_scene_settings.py | 2 +- 3 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/extract_local_render.py diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index baac64ed0c..be024b7e24 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -1,10 +1,14 @@ -from openpype.lib import abstract_collect_render -from openpype.lib.abstract_collect_render import RenderInstance -import pyblish.api -import attr import os +import re +import attr +import tempfile from avalon import aftereffects +import pyblish.api + +from openpype.settings import get_project_settings +from openpype.lib import abstract_collect_render +from openpype.lib.abstract_collect_render import RenderInstance @attr.s @@ -13,6 +17,8 @@ class AERenderInstance(RenderInstance): comp_name = attr.ib(default=None) comp_id = attr.ib(default=None) fps = attr.ib(default=None) + projectEntity = attr.ib(default=None) + stagingDir = attr.ib(default=None) class CollectAERender(abstract_collect_render.AbstractCollectRender): @@ -21,6 +27,11 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): label = "Collect After Effects Render Layers" hosts = ["aftereffects"] + # internal + family_remapping = { + "render": ("render.farm", "farm"), # (family, label) + "renderLocal": ("render", "local") + } padding_width = 6 rendered_extension = 'png' @@ -62,14 +73,16 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): fps = work_area_info.frameRate # TODO add resolution when supported by extension - if inst["family"] == "render" and inst["active"]: + if inst["family"] in self.family_remapping.keys() \ + and inst["active"]: + remapped_family = self.family_remapping[inst["family"]] instance = AERenderInstance( - family="render.farm", # other way integrate would catch it - families=["render.farm"], + family=remapped_family[0], + families=[remapped_family[0]], version=version, time="", source=current_file, - label="{} - farm".format(inst["subset"]), + label="{} - {}".format(inst["subset"], remapped_family[1]), subset=inst["subset"], asset=context.data["assetEntity"]["name"], attachTo=False, @@ -105,6 +118,30 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): instance.outputDir = self._get_output_dir(instance) + settings = get_project_settings(os.getenv("AVALON_PROJECT")) + reviewable_subset_filter = \ + (settings["deadline"] + ["publish"] + ["ProcessSubmittedJobOnFarm"] + ["aov_filter"]) + + if inst["family"] == "renderLocal": + # for local renders + instance.anatomyData["version"] = instance.version + instance.anatomyData["subset"] = instance.subset + instance.stagingDir = tempfile.mkdtemp() + instance.projectEntity = project_entity + + if self.hosts[0] in reviewable_subset_filter.keys(): + for aov_pattern in \ + reviewable_subset_filter[self.hosts[0]]: + if re.match(aov_pattern, instance.subset): + instance.families.append("review") + instance.review = True + break + + self.log.info("New instance:: {}".format(instance)) + instances.append(instance) return instances diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py new file mode 100644 index 0000000000..0f82961bdf --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -0,0 +1,69 @@ +import os + +import openpype.api +from avalon import aftereffects + + +class ExtractLocalRender(openpype.api.Extractor): + """Render RenderQueue locally.""" + + order = openpype.api.Extractor.order - 0.47 + label = "Extract Local Render" + hosts = ["aftereffects"] + families = ["render"] + + def process(self, instance): + stub = aftereffects.stub() + staging_dir = instance.data["stagingDir"] + + stub.render(staging_dir) + + # pull file name from Render Queue Output module + render_q = stub.get_render_info() + if not render_q: + raise ValueError("No file extension set in Render Queue") + _, ext = os.path.splitext(os.path.basename(render_q.file_name)) + ext = ext[1:] + + first_file_path = None + files = [] + for file_name in os.listdir(staging_dir): + files.append(file_name) + if first_file_path is None: + first_file_path = os.path.join(staging_dir, + file_name) + + repre_data = { + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + "name": ext, + "ext": ext, + "files": files, + "stagingDir": staging_dir + } + if instance.data["review"]: + repre_data["preview"] = True, + repre_data["tags"] = ["review", "ftrackreview"] + + instance.data["representations"] = [repre_data] + + ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") + # Generate thumbnail. + thumbnail_path = os.path.join(staging_dir, + "thumbnail.jpg") + args = [ + ffmpeg_path, "-y", + "-i", first_file_path, + "-vf", "scale=300:-1", + "-vframes", "1", + thumbnail_path + ] + output = openpype.lib.run_subprocess(args) + + instance.data["representations"].append({ + "name": "thumbnail", + "ext": "jpg", + "files": os.path.basename(thumbnail_path), + "stagingDir": staging_dir, + "tags": ["thumbnail"] + }) diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py index 5301a2f3ea..7fba11957c 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py @@ -53,7 +53,7 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Scene Settings" - families = ["render.farm"] + families = ["render.farm", "render"] hosts = ["aftereffects"] optional = True From ce6067c883d79bf678fdef2ceb9dbbd8dff83ab6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Aug 2021 09:18:28 +0200 Subject: [PATCH 295/333] #122 - AE local - added local render functionality to documentation --- website/docs/artist_hosts_aftereffects.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index 879c0d4646..a9026ce2d4 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -22,7 +22,7 @@ Drag extension.zxp and drop it to Anastasyi's Extension Manager. The extension w ## Implemented functionality AfterEffects implementation currently allows you to import and add various media to composition (image plates, renders, audio files, video files etc.) -and send prepared composition for rendering to Deadline. +and send prepared composition for rendering to Deadline or render locally. ## Usage @@ -53,6 +53,10 @@ will be changed. ### Publish +#### RenderQueue + +AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue. Currently its expected to have only single render item and single output module in the Render Queue. + When you are ready to share your work, you will need to publish it. This is done by opening the `Publish` by clicking the corresponding button in the OpenPype Panel. ![Publish](assets/aftereffects_publish.png) From 1a1559004157d94d717acd580b3013f02cc2bc47 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Aug 2021 09:26:35 +0200 Subject: [PATCH 296/333] #122 - AE local - added local render functionality to documentation --- website/docs/artist_hosts_aftereffects.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_aftereffects.md b/website/docs/artist_hosts_aftereffects.md index a9026ce2d4..fffc6302b7 100644 --- a/website/docs/artist_hosts_aftereffects.md +++ b/website/docs/artist_hosts_aftereffects.md @@ -55,7 +55,9 @@ will be changed. #### RenderQueue -AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue. Currently its expected to have only single render item and single output module in the Render Queue. +AE's Render Queue is required for publishing locally or on a farm. Artist needs to configure expected result format (extension, resolution) in the Render Queue in an Output module. Currently its expected to have only single render item and single output module in the Render Queue. + +AE might throw some warning windows during publishing locally, so please pay attention to them in a case publishing seems to be stuck in a `Extract Local Render`. When you are ready to share your work, you will need to publish it. This is done by opening the `Publish` by clicking the corresponding button in the OpenPype Panel. From 13197d01d7e37706ddff2489a33c71a9b4a358a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 5 Aug 2021 10:45:17 +0200 Subject: [PATCH 297/333] h264 cares about bitrate --- openpype/scripts/otio_burnin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index ca77171981..dc8d60cb37 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -113,6 +113,10 @@ def _h264_codec_args(ffprobe_data): output.extend(["-codec:v", "h264"]) + bit_rate = ffprobe_data.get("bit_rate") + if bit_rate: + output.extend(["-b:v", bit_rate]) + pix_fmt = ffprobe_data.get("pix_fmt") if pix_fmt: output.extend(["-pix_fmt", pix_fmt]) From 9fa69dd43c51f1c41bf1c8a259ec7812cdd9c331 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 5 Aug 2021 12:16:32 +0200 Subject: [PATCH 298/333] Hiero: loaded clip was not set colorspace from version data --- openpype/hosts/hiero/plugins/load/load_clip.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index 9e12fa360e..fa666207c5 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -54,6 +54,9 @@ class LoadClip(phiero.SequenceLoader): object_name = self.clip_name_template.format( **context["representation"]["context"]) + # set colorspace + track_item.source().setSourceMediaColourTransform(colorspace) + # add additional metadata from the version to imprint Avalon knob add_keys = [ "frameStart", "frameEnd", "source", "author", @@ -109,9 +112,13 @@ class LoadClip(phiero.SequenceLoader): colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) file = api.get_representation_path(representation).replace("\\", "/") + clip = track_item.source() # reconnect media to new path - track_item.source().reconnectMedia(file) + clip.reconnectMedia(file) + + # set colorspace + clip.setSourceMediaColourTransform(colorspace) # add additional metadata from the version to imprint Avalon knob add_keys = [ @@ -160,6 +167,7 @@ class LoadClip(phiero.SequenceLoader): @classmethod def set_item_color(cls, track_item, version): + clip = track_item.source() # define version name version_name = version.get("name", None) # get all versions in list @@ -172,6 +180,6 @@ class LoadClip(phiero.SequenceLoader): # set clip colour if version_name == max_version: - track_item.source().binItem().setColor(cls.clip_color_last) + clip.binItem().setColor(cls.clip_color_last) else: - track_item.source().binItem().setColor(cls.clip_color) + clip.binItem().setColor(cls.clip_color) From dbb85a40d7eac2ffacf8d49a19bb53a81aef711e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 5 Aug 2021 12:23:33 +0200 Subject: [PATCH 299/333] don't skip instances if is in collecting stage --- openpype/tools/pyblish_pype/control.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/pyblish_pype/control.py b/openpype/tools/pyblish_pype/control.py index ae9ca40be5..234135fd9a 100644 --- a/openpype/tools/pyblish_pype/control.py +++ b/openpype/tools/pyblish_pype/control.py @@ -316,6 +316,7 @@ class Controller(QtCore.QObject): self.was_skipped.emit(plugin) continue + in_collect_stage = self.collect_state == 0 if plugin.__instanceEnabled__: instances = pyblish.logic.instances_by_plugin( self.context, plugin @@ -325,7 +326,10 @@ class Controller(QtCore.QObject): continue for instance in instances: - if instance.data.get("publish") is False: + if ( + not in_collect_stage + and instance.data.get("publish") is False + ): pyblish.logic.log.debug( "%s was inactive, skipping.." % instance ) @@ -338,7 +342,7 @@ class Controller(QtCore.QObject): yield (plugin, instance) else: families = util.collect_families_from_instances( - self.context, only_active=True + self.context, only_active=not in_collect_stage ) plugins = pyblish.logic.plugins_by_families( [plugin], families From 1b76d8bf8852b5828114ace04ce187b10ae2602a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 5 Aug 2021 12:23:43 +0200 Subject: [PATCH 300/333] make plugins compatible if were processed --- openpype/tools/pyblish_pype/model.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/pyblish_pype/model.py b/openpype/tools/pyblish_pype/model.py index 50ba27166b..bb1aff2a9a 100644 --- a/openpype/tools/pyblish_pype/model.py +++ b/openpype/tools/pyblish_pype/model.py @@ -498,6 +498,9 @@ class PluginModel(QtGui.QStandardItemModel): ): new_flag_states[PluginStates.HasError] = True + if not publish_states & PluginStates.IsCompatible: + new_flag_states[PluginStates.IsCompatible] = True + item.setData(new_flag_states, Roles.PublishFlagsRole) records = item.data(Roles.LogRecordsRole) or [] From ee3156ce2672e45550309e16c543ce76b8a6eeb6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Aug 2021 10:36:38 +0200 Subject: [PATCH 301/333] #122 - AE local - better logging for thumbnail extraction --- .../plugins/publish/extract_local_render.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index 0f82961bdf..10e5ad5c72 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -1,4 +1,6 @@ import os +import six +import sys import openpype.api from avalon import aftereffects @@ -51,6 +53,7 @@ class ExtractLocalRender(openpype.api.Extractor): # Generate thumbnail. thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") + args = [ ffmpeg_path, "-y", "-i", first_file_path, @@ -58,7 +61,12 @@ class ExtractLocalRender(openpype.api.Extractor): "-vframes", "1", thumbnail_path ] - output = openpype.lib.run_subprocess(args) + self.log.debug("Thumbnail args:: {}".format(args)) + try: + output = openpype.lib.run_subprocess(args) + except TypeError: + self.log.warning("Error in creating thumbnail") + six.reraise(*sys.exc_info()) instance.data["representations"].append({ "name": "thumbnail", From d2b13996dae5258d552daf9648ae0f48d43344df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Aug 2021 12:00:18 +0200 Subject: [PATCH 302/333] #122 - AE local - better logging for thumbnail extraction --- .../hosts/aftereffects/plugins/publish/extract_local_render.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index 10e5ad5c72..bf2f227d6c 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -17,6 +17,7 @@ class ExtractLocalRender(openpype.api.Extractor): def process(self, instance): stub = aftereffects.stub() staging_dir = instance.data["stagingDir"] + self.log.info("staging_dir::{}".format(staging_dir)) stub.render(staging_dir) @@ -29,6 +30,7 @@ class ExtractLocalRender(openpype.api.Extractor): first_file_path = None files = [] + self.log.info("files::{}".format(os.listdir(staging_dir))) for file_name in os.listdir(staging_dir): files.append(file_name) if first_file_path is None: From 5b7aa3717ff915c25f850e301c5bff2fa4c5eef3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 6 Aug 2021 12:04:35 +0200 Subject: [PATCH 303/333] row order changes are propagated right way with right buttons --- .../settings/settings/list_item_widget.py | 186 ++++++++++-------- 1 file changed, 107 insertions(+), 79 deletions(-) diff --git a/openpype/tools/settings/settings/list_item_widget.py b/openpype/tools/settings/settings/list_item_widget.py index 82ca541132..c9df5caf01 100644 --- a/openpype/tools/settings/settings/list_item_widget.py +++ b/openpype/tools/settings/settings/list_item_widget.py @@ -117,6 +117,9 @@ class ListItem(QtWidgets.QWidget): self.spacer_widget = spacer_widget + self._row = -1 + self._is_last = False + @property def category_widget(self): return self.entity_widget.category_widget @@ -136,28 +139,40 @@ class ListItem(QtWidgets.QWidget): def add_widget_to_layout(self, widget, label=None): self.content_layout.addWidget(widget, 1) + def set_row(self, row, is_last): + if row == self._row and is_last == self._is_last: + return + + trigger_order_changed = ( + row != self._row + or is_last != self._is_last + ) + self._row = row + self._is_last = is_last + + if trigger_order_changed: + self.order_changed() + + @property def row(self): - return self.entity_widget.input_fields.index(self) + return self._row def parent_rows_count(self): return len(self.entity_widget.input_fields) def _on_add_clicked(self): - self.entity_widget.add_new_item(row=self.row() + 1) + self.entity_widget.add_new_item(row=self.row + 1) def _on_remove_clicked(self): self.entity_widget.remove_row(self) def _on_up_clicked(self): - row = self.row() - self.entity_widget.swap_rows(row - 1, row) + self.entity_widget.swap_rows(self.row - 1, self.row) def _on_down_clicked(self): - row = self.row() - self.entity_widget.swap_rows(row, row + 1) + self.entity_widget.swap_rows(self.row, self.row + 1) def order_changed(self): - row = self.row() parent_row_count = self.parent_rows_count() if parent_row_count == 1: self.up_btn.setVisible(False) @@ -168,11 +183,11 @@ class ListItem(QtWidgets.QWidget): self.up_btn.setVisible(True) self.down_btn.setVisible(True) - if row == 0: + if self.row == 0: self.up_btn.setEnabled(False) self.down_btn.setEnabled(True) - elif row == parent_row_count - 1: + elif self.row == parent_row_count - 1: self.up_btn.setEnabled(True) self.down_btn.setEnabled(False) @@ -191,6 +206,7 @@ class ListWidget(InputWidget): def create_ui(self): self._child_style_state = "" self.input_fields = [] + self._input_fields_by_entity_id = {} main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -243,8 +259,7 @@ class ListWidget(InputWidget): self.entity_widget.add_widget_to_layout(self, entity_label) def set_entity_value(self): - for input_field in tuple(self.input_fields): - self.remove_row(input_field) + self.remove_all_rows() for entity in self.entity.children: self.add_row(entity) @@ -262,39 +277,60 @@ class ListWidget(InputWidget): def _on_entity_change(self): # TODO do less inefficient - input_field_last_idx = len(self.input_fields) - 1 - child_len = len(self.entity) + childen_order = [] + new_children = [] for idx, child_entity in enumerate(self.entity): - if idx > input_field_last_idx: - self.add_row(child_entity, idx) - input_field_last_idx += 1 + input_field = self._input_fields_by_entity_id.get(child_entity.id) + if input_field is not None: + childen_order.append(input_field) + else: + new_children.append((idx, child_entity)) + + order_changed = False + for idx, input_field in enumerate(childen_order): + current_field = self.input_fields[idx] + if current_field is input_field: continue + order_changed = True + old_idx = self.input_fields.index(input_field) + self.input_fields[old_idx], self.input_fields[idx] = ( + current_field, input_field + ) + self.content_layout.insertWidget(idx + 1, input_field) - if self.input_fields[idx].entity is child_entity: - continue + kept_len = len(childen_order) + fields_len = len(self.input_fields) + if fields_len > kept_len: + order_changed = True + for row in reversed(range(kept_len, fields_len)): + self.remove_row(row=row) - input_field_idx = None - for _input_field_idx, input_field in enumerate(self.input_fields): - if input_field.entity is child_entity: - input_field_idx = _input_field_idx - break + for idx, child_entity in new_children: + order_changed = False + self.add_row(child_entity, idx) - if input_field_idx is None: - self.add_row(child_entity, idx) - input_field_last_idx += 1 - continue + if not order_changed: + return - input_field = self.input_fields.pop(input_field_idx) - self.input_fields.insert(idx, input_field) - self.content_layout.insertWidget(idx, input_field) + self._on_order_change() - new_input_field_len = len(self.input_fields) - if child_len != new_input_field_len: - for _idx in range(child_len, new_input_field_len): - # Remove row at the same index - self.remove_row(self.input_fields[child_len]) + input_field_len = self.count() + self.empty_row.setVisible(input_field_len == 0) - self.empty_row.setVisible(self.count() == 0) + def _on_order_change(self): + last_idx = self.count() - 1 + previous_input = None + for idx, input_field in enumerate(self.input_fields): + input_field.set_row(idx, idx == last_idx) + next_input = input_field.input_field.focusProxy() + if previous_input is not None: + self.setTabOrder(previous_input, next_input) + else: + self.setTabOrder(self, next_input) + previous_input = next_input + + if previous_input is not None: + self.setTabOrder(previous_input, self) def count(self): return len(self.input_fields) @@ -307,32 +343,20 @@ class ListWidget(InputWidget): def add_new_item(self, row=None): new_entity = self.entity.add_new_item(row) - for input_field in self.input_fields: - if input_field.entity is new_entity: - input_field.input_field.setFocus(True) - break + input_field = self._input_fields_by_entity_id.get(new_entity.id) + if input_field is not None: + input_field.input_field.setFocus(True) return new_entity def add_row(self, child_entity, row=None): # Create new item item_widget = ListItem(child_entity, self) - - previous_field = None - next_field = None + self._input_fields_by_entity_id[child_entity.id] = item_widget if row is None: - if self.input_fields: - previous_field = self.input_fields[-1] self.content_layout.addWidget(item_widget) self.input_fields.append(item_widget) else: - if row > 0: - previous_field = self.input_fields[row - 1] - - max_index = self.count() - if row < max_index: - next_field = self.input_fields[row] - self.content_layout.insertWidget(row + 1, item_widget) self.input_fields.insert(row, item_widget) @@ -342,49 +366,53 @@ class ListWidget(InputWidget): # added as widget here which won't because is not in input_fields item_widget.input_field.set_entity_value() - if previous_field: - previous_field.order_changed() + self._on_order_change() - if next_field: - next_field.order_changed() - - item_widget.order_changed() - - previous_input = None - for input_field in self.input_fields: - if previous_input is not None: - self.setTabOrder( - previous_input, input_field.input_field.focusProxy() - ) - previous_input = input_field.input_field.focusProxy() + input_field_len = self.count() + self.empty_row.setVisible(input_field_len == 0) self.updateGeometry() - def remove_row(self, item_widget): - row = self.input_fields.index(item_widget) - previous_field = None - next_field = None - if row > 0: - previous_field = self.input_fields[row - 1] + def remove_all_rows(self): + self._input_fields_by_entity_id = {} + while self.input_fields: + item_widget = self.input_fields.pop(0) + self.content_layout.removeWidget(item_widget) + item_widget.setParent(None) + item_widget.deleteLater() - if row != len(self.input_fields) - 1: - next_field = self.input_fields[row + 1] + self.empty_row.setVisible(True) + + self.updateGeometry() + + def remove_row(self, item_widget=None, row=None): + if item_widget is None: + item_widget = self.input_fields[row] + elif row is None: + row = self.input_fields.index(item_widget) self.content_layout.removeWidget(item_widget) self.input_fields.pop(row) + self._input_fields_by_entity_id.pop(item_widget.entity.id) item_widget.setParent(None) item_widget.deleteLater() if item_widget.entity in self.entity: self.entity.remove(item_widget.entity) - if previous_field: - previous_field.order_changed() + rows = self.count() + any_item = rows == 0 + if any_item: + start_row = 0 + if row > 0: + start_row = row - 1 - if next_field: - next_field.order_changed() + last_row = rows - 1 + _enum = enumerate(self.input_fields[start_row:rows]) + for idx, _item_widget in _enum: + _item_widget.set_row(idx, idx == last_row) - self.empty_row.setVisible(self.count() == 0) + self.empty_row.setVisible(any_item) self.updateGeometry() From abdfe4ee38de2589400aa030e52fb11c795b749d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Aug 2021 12:47:39 +0200 Subject: [PATCH 304/333] AE local render - fixed single render file --- .../aftereffects/plugins/publish/extract_local_render.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index bf2f227d6c..a3858112c3 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -37,12 +37,16 @@ class ExtractLocalRender(openpype.api.Extractor): first_file_path = os.path.join(staging_dir, file_name) + resulting_files = files + if len(files) == 1: + resulting_files = files[0] + repre_data = { "frameStart": instance.data["frameStart"], "frameEnd": instance.data["frameEnd"], "name": ext, "ext": ext, - "files": files, + "files": resulting_files, "stagingDir": staging_dir } if instance.data["review"]: From 66dc345e681b4156e80c0e9374794144952fe081 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Aug 2021 14:47:11 +0200 Subject: [PATCH 305/333] AE local render - add create review and burnin --- openpype/plugins/publish/extract_burnin.py | 3 ++- openpype/plugins/publish/extract_review.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_burnin.py b/openpype/plugins/publish/extract_burnin.py index ef52d51325..91e0a0f3ec 100644 --- a/openpype/plugins/publish/extract_burnin.py +++ b/openpype/plugins/publish/extract_burnin.py @@ -44,7 +44,8 @@ class ExtractBurnin(openpype.api.Extractor): "harmony", "fusion", "aftereffects", - "tvpaint" + "tvpaint", + "aftereffects" # "resolve" ] optional = True diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index de54b554e3..bdcd3b8e60 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -44,7 +44,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "standalonepublisher", "fusion", "tvpaint", - "resolve" + "resolve", + "aftereffects" ] # Supported extensions From 14bfd47b6925a630d75aaeff394e80a2154f027c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Aug 2021 16:42:19 +0200 Subject: [PATCH 306/333] AE local render - fix - ftrackreview shouldn't be on sequence representation --- .../hosts/aftereffects/plugins/publish/extract_local_render.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index a3858112c3..37337e7fee 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -50,8 +50,7 @@ class ExtractLocalRender(openpype.api.Extractor): "stagingDir": staging_dir } if instance.data["review"]: - repre_data["preview"] = True, - repre_data["tags"] = ["review", "ftrackreview"] + repre_data["tags"] = ["review"] instance.data["representations"] = [repre_data] From 9e8e8ec656de4442a83a8753a9e8a43610bc3af5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 6 Aug 2021 18:13:25 +0200 Subject: [PATCH 307/333] hiero: fix colorspace attribute distribution --- openpype/hosts/hiero/plugins/load/load_clip.py | 6 ++++-- .../hiero/plugins/publish/precollect_instances.py | 14 +++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index fa666207c5..b905dd4431 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -55,7 +55,8 @@ class LoadClip(phiero.SequenceLoader): **context["representation"]["context"]) # set colorspace - track_item.source().setSourceMediaColourTransform(colorspace) + if colorspace: + track_item.source().setSourceMediaColourTransform(colorspace) # add additional metadata from the version to imprint Avalon knob add_keys = [ @@ -118,7 +119,8 @@ class LoadClip(phiero.SequenceLoader): clip.reconnectMedia(file) # set colorspace - clip.setSourceMediaColourTransform(colorspace) + if colorspace: + clip.setSourceMediaColourTransform(colorspace) # add additional metadata from the version to imprint Avalon knob add_keys = [ diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 4984849aa7..9b529edf88 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -120,6 +120,13 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # create instance instance = context.create_instance(**data) + # add colorspace data + instance.data.update({ + "versionData": { + "colorspace": track_item.sourceMediaColourTransform(), + } + }) + # create shot instance for shot attributes create/update self.create_shot_instance(context, **data) @@ -133,13 +140,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # create audio subset instance self.create_audio_instance(context, **data) - # add colorspace data - instance.data.update({ - "versionData": { - "colorspace": track_item.sourceMediaColourTransform(), - } - }) - # add audioReview attribute to plate instance data # if reviewTrack is on if tag_data.get("reviewTrack") is not None: From 68baae0b68223011ad2a7dab5d19705b0674e60e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 7 Aug 2021 03:40:49 +0000 Subject: [PATCH 308/333] [Automated] Bump version --- CHANGELOG.md | 17 ++++++++++------- openpype/version.py | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3f2150c8..964120330e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ # Changelog -## [3.3.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) **🚀 Enhancements** +- Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) +- Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900) +- Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899) +- Support nested studio plugins paths. [\#1898](https://github.com/pypeclub/OpenPype/pull/1898) - Settings: global validators with options [\#1892](https://github.com/pypeclub/OpenPype/pull/1892) - Settings: Conditional dict enum positioning [\#1891](https://github.com/pypeclub/OpenPype/pull/1891) - Expose stop timer through rest api. [\#1886](https://github.com/pypeclub/OpenPype/pull/1886) @@ -25,11 +29,12 @@ - nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829) - Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823) - Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) -- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) - Maya: Shader name validation [\#1762](https://github.com/pypeclub/OpenPype/pull/1762) **🐛 Bug fixes** +- Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903) +- Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902) - Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893) - global: integrate name missing default template [\#1890](https://github.com/pypeclub/OpenPype/pull/1890) - publisher: editorial plugins fixes [\#1889](https://github.com/pypeclub/OpenPype/pull/1889) @@ -44,6 +49,7 @@ - Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Failsafe for cross project containers. [\#1806](https://github.com/pypeclub/OpenPype/pull/1806) - Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) +- Settings error dialog on show [\#1798](https://github.com/pypeclub/OpenPype/pull/1798) **Merged pull requests:** @@ -59,6 +65,7 @@ - Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) - Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799) +- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) - Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) - Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) - Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776) @@ -68,7 +75,6 @@ - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) - Deadline: Nuke submission additional attributes [\#1756](https://github.com/pypeclub/OpenPype/pull/1756) - Settings schema without prefill [\#1753](https://github.com/pypeclub/OpenPype/pull/1753) -- Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) **🐛 Bug fixes** @@ -76,6 +82,7 @@ - Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) - Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) - Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) +- Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) - FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775) - Fix - single file files are str only, cast it to list to count properly [\#1772](https://github.com/pypeclub/OpenPype/pull/1772) - Environments in app executable for MacOS [\#1768](https://github.com/pypeclub/OpenPype/pull/1768) @@ -84,10 +91,6 @@ - Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761) - Anatomy others templates don't cause crash [\#1758](https://github.com/pypeclub/OpenPype/pull/1758) - Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745) -- hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743) -- Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) -- Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741) -- StandalonePublisher: failing collector for editorial [\#1738](https://github.com/pypeclub/OpenPype/pull/1738) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 473be3bafc..c888e5f9d9 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.7" +__version__ = "3.3.0-nightly.8" From 7b71e1237a13e49b2da86ba7c664dd038244a458 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 9 Aug 2021 12:22:01 +0200 Subject: [PATCH 309/333] submodules: avalon-core update --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index cfd4191e36..e5c8a15fde 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit cfd4191e364b47de7364096f45d9d9d9a901692a +Subproject commit e5c8a15fde77708c924eab3018bda255f17b5390 From a4f9ee1496f34f94d5e476b6614ecb772784fd93 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Aug 2021 15:24:56 +0200 Subject: [PATCH 310/333] Fix - texture validators for workfiles triggers only for textures workfiles It was triggering for all workfiles --- .../standalonepublisher/plugins/publish/collect_texture.py | 1 + .../plugins/publish/validate_texture_batch.py | 2 +- .../plugins/publish/validate_texture_name.py | 2 +- .../plugins/publish/validate_texture_workfiles.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py index d70a0a75b8..596a8ccfd2 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_texture.py @@ -270,6 +270,7 @@ class CollectTextures(pyblish.api.ContextPlugin): # store origin if family == 'workfile': families = self.workfile_families + families.append("texture_batch_workfile") new_instance.data["source"] = "standalone publisher" else: diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py index af200b59e0..d592a4a059 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_batch.py @@ -8,7 +8,7 @@ class ValidateTextureBatch(pyblish.api.InstancePlugin): label = "Validate Texture Presence" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder - families = ["workfile"] + families = ["texture_batch_workfile"] optional = False def process(self, instance): diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py index 92f930c3fc..f210be3631 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_name.py @@ -8,7 +8,7 @@ class ValidateTextureBatchNaming(pyblish.api.InstancePlugin): label = "Validate Texture Batch Naming" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder - families = ["workfile", "textures"] + families = ["texture_batch_workfile", "textures"] optional = False def process(self, instance): diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py index aa3aad71db..25bb5aea4a 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_texture_workfiles.py @@ -11,7 +11,7 @@ class ValidateTextureBatchWorkfiles(pyblish.api.InstancePlugin): label = "Validate Texture Workfile Has Resources" hosts = ["standalonepublisher"] order = openpype.api.ValidateContentsOrder - families = ["workfile"] + families = ["texture_batch_workfile"] optional = True # from presets From c386fc340dcfe02f5a0d6c440b9ab8040052b51f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Aug 2021 12:15:31 +0200 Subject: [PATCH 311/333] Nuke: update video file crassing --- openpype/hosts/nuke/plugins/load/load_mov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_mov.py b/openpype/hosts/nuke/plugins/load/load_mov.py index d84c3d4c71..95f20b305f 100644 --- a/openpype/hosts/nuke/plugins/load/load_mov.py +++ b/openpype/hosts/nuke/plugins/load/load_mov.py @@ -259,7 +259,7 @@ class LoadMov(api.Loader): read_node["last"].setValue(last) read_node['frame_mode'].setValue("start at") - if int(self.first_frame) == int(read_node['frame'].value()): + if int(float(self.first_frame)) == int(float(read_node['frame'].value())): # start at workfile start read_node['frame'].setValue(str(self.first_frame)) else: From 3be9fa8cf170184c9ed0003a024d503fb77956bc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Aug 2021 12:20:06 +0200 Subject: [PATCH 312/333] hound: pep8 --- openpype/hosts/nuke/plugins/load/load_mov.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/load/load_mov.py b/openpype/hosts/nuke/plugins/load/load_mov.py index 95f20b305f..f7523d0a6e 100644 --- a/openpype/hosts/nuke/plugins/load/load_mov.py +++ b/openpype/hosts/nuke/plugins/load/load_mov.py @@ -259,7 +259,8 @@ class LoadMov(api.Loader): read_node["last"].setValue(last) read_node['frame_mode'].setValue("start at") - if int(float(self.first_frame)) == int(float(read_node['frame'].value())): + if int(float(self.first_frame)) == int( + float(read_node['frame'].value())): # start at workfile start read_node['frame'].setValue(str(self.first_frame)) else: From 29b1334f911d515bf7f678dfcd9730acce7d0214 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Aug 2021 14:11:33 +0200 Subject: [PATCH 313/333] standalone: validator asset parents --- openpype/plugins/publish/validate_editorial_asset_name.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index f13e3b4f38..28344af98e 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -14,8 +14,10 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): label = "Validate Editorial Asset Name" def process(self, context): + project_entity = context.data["projectEntity"] asset_and_parents = self.get_parents(context) + self.log.debug("__ asset_and_parents: {}".format(asset_and_parents)) if not io.Session: io.install() @@ -25,7 +27,8 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): self.log.debug("__ db_assets: {}".format(db_assets)) asset_db_docs = { - str(e["name"]): e["data"]["parents"] for e in db_assets} + str(e["name"]): [project_entity["name"]] + e["data"]["parents"] + for e in db_assets} self.log.debug("__ project_entities: {}".format( pformat(asset_db_docs))) From 52f4a877cfcb227fb287f64a16d0578d88c6718b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 10 Aug 2021 15:24:32 +0200 Subject: [PATCH 314/333] standalone: systematic fix rather then workaround patch addressing issue https://github.com/pypeclub/OpenPype/issues/1918 --- openpype/plugins/publish/validate_editorial_asset_name.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index 28344af98e..eebba61af3 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -14,7 +14,6 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): label = "Validate Editorial Asset Name" def process(self, context): - project_entity = context.data["projectEntity"] asset_and_parents = self.get_parents(context) self.log.debug("__ asset_and_parents: {}".format(asset_and_parents)) @@ -27,7 +26,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): self.log.debug("__ db_assets: {}".format(db_assets)) asset_db_docs = { - str(e["name"]): [project_entity["name"]] + e["data"]["parents"] + str(e["name"]): e["data"]["parents"] for e in db_assets} self.log.debug("__ project_entities: {}".format( @@ -110,6 +109,7 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): parents = instance.data["parents"] return_dict.update({ - asset: [p["entity_name"] for p in parents] + asset: [p["entity_name"] for p in parents + if p["entity_type"].lower() != "project"] }) return return_dict From 90b945215242202de38a780e8ec96230b9ef1ed7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 16:01:26 +0200 Subject: [PATCH 315/333] dont use spacer widget --- openpype/tools/settings/settings/dict_mutable_widget.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index df6525e86a..833b7ac4d2 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -5,8 +5,7 @@ from Qt import QtWidgets, QtCore from .base import BaseWidget from .widgets import ( ExpandingWidget, - IconButton, - SpacerWidget + IconButton ) from openpype.tools.settings import ( BTN_FIXED_SIZE, @@ -61,7 +60,6 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): def create_addible_ui(self): add_btn = create_add_btn(self) remove_btn = create_remove_btn(self) - spacer_widget = SpacerWidget(self) remove_btn.setEnabled(False) @@ -70,13 +68,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): layout.setSpacing(3) layout.addWidget(add_btn, 0) layout.addWidget(remove_btn, 0) - layout.addWidget(spacer_widget, 1) + layout.addStretch(1) add_btn.clicked.connect(self._on_add_clicked) self.add_btn = add_btn self.remove_btn = remove_btn - self.spacer_widget = spacer_widget def _on_focus_lose(self): if self.key_input.hasFocus() or self.key_label_input.hasFocus(): From b8be1371ba7f6800650b1129da0c544f131fa618 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 16:01:40 +0200 Subject: [PATCH 316/333] swap key and label in item label --- openpype/tools/settings/settings/dict_mutable_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 833b7ac4d2..f8436769f5 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -431,7 +431,7 @@ class ModifiableDictItem(QtWidgets.QWidget): key_value = self.key_input.text() key_label_value = self.key_label_input.text() if key_label_value: - label = "{} ({})".format(key_label_value, key_value) + label = "{} ({})".format(key_value, key_label_value) else: label = key_value self.wrapper_widget.label_widget.setText(label) From b53c452a9b76ad41004637df917c88b65c586b1c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 17:29:08 +0200 Subject: [PATCH 317/333] added confirmation button to modifiable dict item --- .../settings/settings/dict_mutable_widget.py | 109 +++++++++++++++++- 1 file changed, 107 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index f8436769f5..74a7a9793b 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -1,6 +1,6 @@ from uuid import uuid4 -from Qt import QtWidgets, QtCore +from Qt import QtWidgets, QtCore, QtGui from .base import BaseWidget from .widgets import ( @@ -14,6 +14,63 @@ from openpype.tools.settings import ( from openpype.settings.constants import KEY_REGEX +class PaintHelper: + cached_icons = {} + + @classmethod + def _draw_image(cls, width, height, brush): + image = QtGui.QPixmap(width, height) + image.fill(QtCore.Qt.transparent) + + icon_path_stroker = QtGui.QPainterPathStroker() + icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap) + icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin) + icon_path_stroker.setWidth(height / 5) + + painter = QtGui.QPainter(image) + painter.setPen(QtCore.Qt.transparent) + painter.setBrush(brush) + rect = QtCore.QRect(0, 0, image.width(), image.height()) + fifteenth = rect.height() / 15 + # Left point + p1 = QtCore.QPoint( + rect.x() + (5 * fifteenth), + rect.y() + (9 * fifteenth) + ) + # Middle bottom point + p2 = QtCore.QPoint( + rect.center().x(), + rect.y() + (11 * fifteenth) + ) + # Top right point + p3 = QtCore.QPoint( + rect.x() + (10 * fifteenth), + rect.y() + (5 * fifteenth) + ) + + path = QtGui.QPainterPath(p1) + path.lineTo(p2) + path.lineTo(p3) + + stroked_path = icon_path_stroker.createStroke(path) + painter.drawPath(stroked_path) + + painter.end() + + return image + + @classmethod + def get_confirm_icon(cls, width, height): + key = "{}x{}-confirm_image".format(width, height) + icon = cls.cached_icons.get(key) + + if icon is None: + image = cls._draw_image(width, height, QtCore.Qt.white) + icon = QtGui.QIcon(image) + cls.cached_icons[key] = icon + return icon + + def create_add_btn(parent): add_btn = QtWidgets.QPushButton("+", parent) add_btn.setFocusPolicy(QtCore.Qt.ClickFocus) @@ -30,6 +87,19 @@ def create_remove_btn(parent): return remove_btn +def create_confirm_btn(parent): + confirm_btn = QtWidgets.QPushButton(parent) + + icon = PaintHelper.get_confirm_icon( + BTN_FIXED_SIZE, BTN_FIXED_SIZE + ) + confirm_btn.setIcon(icon) + confirm_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + confirm_btn.setProperty("btn-type", "tool-item") + confirm_btn.setFixedSize(BTN_FIXED_SIZE, BTN_FIXED_SIZE) + return confirm_btn + + class ModifiableDictEmptyItem(QtWidgets.QWidget): def __init__(self, entity_widget, store_as_list, parent): super(ModifiableDictEmptyItem, self).__init__(parent) @@ -41,6 +111,8 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): self.is_duplicated = False self.key_is_valid = store_as_list + self.confirm_btn = None + if self.collapsible_key: self.create_collapsible_ui() else: @@ -108,7 +180,16 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): self.is_duplicated = self.entity_widget.is_key_duplicated(key) key_input_state = "" # Collapsible key and empty key are not invalid - if self.collapsible_key and self.key_input.text() == "": + key_value = self.key_input.text() + if self.confirm_btn is not None: + conf_disabled = ( + key_value == "" + or not self.key_is_valid + or self.is_duplicated + ) + self.confirm_btn.setEnabled(not conf_disabled) + + if self.collapsible_key and key_value == "": pass elif self.is_duplicated or not self.key_is_valid: key_input_state = "invalid" @@ -138,11 +219,15 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): key_input_label_widget = QtWidgets.QLabel("Key:", self) key_label_input_label_widget = QtWidgets.QLabel("Label:", self) + confirm_btn = create_confirm_btn(self) + confirm_btn.setEnabled(False) + wrapper_widget = ExpandingWidget("", self) wrapper_widget.add_widget_after_label(key_input_label_widget) wrapper_widget.add_widget_after_label(key_input) wrapper_widget.add_widget_after_label(key_label_input_label_widget) wrapper_widget.add_widget_after_label(key_label_input) + wrapper_widget.add_widget_after_label(confirm_btn) wrapper_widget.hide_toolbox() layout = QtWidgets.QVBoxLayout(self) @@ -154,9 +239,12 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): key_input.returnPressed.connect(self._on_enter_press) key_label_input.returnPressed.connect(self._on_enter_press) + confirm_btn.clicked.connect(self._on_enter_press) + self.key_input = key_input self.key_label_input = key_label_input self.wrapper_widget = wrapper_widget + self.confirm_btn = confirm_btn class ModifiableDictItem(QtWidgets.QWidget): @@ -187,6 +275,8 @@ class ModifiableDictItem(QtWidgets.QWidget): self.key_label_input = None + self.confirm_btn = None + if collapsible_key: self.create_collapsible_ui() else: @@ -274,6 +364,9 @@ class ModifiableDictItem(QtWidgets.QWidget): edit_btn.setProperty("btn-type", "tool-item-icon") edit_btn.setFixedHeight(BTN_FIXED_SIZE) + confirm_btn = create_confirm_btn(self) + confirm_btn.setVisible(False) + remove_btn = create_remove_btn(self) key_input_label_widget = QtWidgets.QLabel("Key:") @@ -283,6 +376,7 @@ class ModifiableDictItem(QtWidgets.QWidget): wrapper_widget.add_widget_after_label(key_input) wrapper_widget.add_widget_after_label(key_label_input_label_widget) wrapper_widget.add_widget_after_label(key_label_input) + wrapper_widget.add_widget_after_label(confirm_btn) wrapper_widget.add_widget_after_label(remove_btn) key_input.textChanged.connect(self._on_key_change) @@ -292,6 +386,7 @@ class ModifiableDictItem(QtWidgets.QWidget): key_label_input.returnPressed.connect(self._on_enter_press) edit_btn.clicked.connect(self.on_edit_pressed) + confirm_btn.clicked.connect(self._on_enter_press) remove_btn.clicked.connect(self.on_remove_clicked) # Hide edit inputs @@ -307,6 +402,7 @@ class ModifiableDictItem(QtWidgets.QWidget): self.key_label_input_label_widget = key_label_input_label_widget self.wrapper_widget = wrapper_widget self.edit_btn = edit_btn + self.confirm_btn = confirm_btn self.remove_btn = remove_btn self.content_widget = content_widget @@ -412,6 +508,14 @@ class ModifiableDictItem(QtWidgets.QWidget): self.temp_key, key, self ) self.temp_key = key + if self.confirm_btn is not None: + conf_disabled = ( + key == "" + or not self.key_is_valid + or is_key_duplicated + ) + self.confirm_btn.setEnabled(not conf_disabled) + if is_key_duplicated or not self.key_is_valid: return @@ -454,6 +558,7 @@ class ModifiableDictItem(QtWidgets.QWidget): self.key_input.setVisible(enabled) self.key_input_label_widget.setVisible(enabled) self.key_label_input.setVisible(enabled) + self.confirm_btn.setVisible(enabled) if not self.is_required: self.remove_btn.setVisible(enabled) if enabled: From 7df70b63d9435387ba37900aaa9c906eb24eef92 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 17:29:14 +0200 Subject: [PATCH 318/333] added key tooltip --- openpype/tools/settings/settings/dict_mutable_widget.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 74a7a9793b..4f2800156c 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -14,6 +14,12 @@ from openpype.tools.settings import ( from openpype.settings.constants import KEY_REGEX +KEY_INPUT_TOOLTIP = ( + "Keys can't be duplicated and may contain alphabetical character (a-Z)" + "\nnumerical characters (0-9) dash (\"-\") or underscore (\"_\")." +) + + class PaintHelper: cached_icons = {} @@ -202,6 +208,7 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): def create_collapsible_ui(self): key_input = QtWidgets.QLineEdit(self) key_input.setObjectName("DictKey") + key_input.setToolTip(KEY_INPUT_TOOLTIP) key_label_input = QtWidgets.QLineEdit(self) @@ -281,6 +288,8 @@ class ModifiableDictItem(QtWidgets.QWidget): self.create_collapsible_ui() else: self.create_addible_ui() + + self.key_input.setToolTip(KEY_INPUT_TOOLTIP) self.update_style() @property From e4334d0e7572feb1f185de95cab85eac878f1fd6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 17:52:34 +0200 Subject: [PATCH 319/333] enum may have defined default item in schema --- openpype/settings/entities/enum_entity.py | 36 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 4f6a2886bc..361ad38dc5 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -73,21 +73,41 @@ class EnumEntity(BaseEnumEntity): def _item_initalization(self): self.multiselection = self.schema_data.get("multiselection", False) self.enum_items = self.schema_data.get("enum_items") + # Default is optional and non breaking attribute + enum_default = self.schema_data.get("default") - valid_keys = set() + all_keys = [] for item in self.enum_items or []: - valid_keys.add(tuple(item.keys())[0]) + key = tuple(item.keys())[0] + all_keys.append(key) - self.valid_keys = valid_keys + self.valid_keys = set(all_keys) if self.multiselection: self.valid_value_types = (list, ) - self.value_on_not_set = [] + value_on_not_set = [] + if enum_default: + if not isinstance(enum_default, list): + enum_default = [enum_default] + + for item in enum_default: + if item in all_keys: + value_on_not_set.append(item) + + self.value_on_not_set = value_on_not_set + else: - for key in valid_keys: - if self.value_on_not_set is NOT_SET: - self.value_on_not_set = key - break + if isinstance(enum_default, list) and enum_default: + enum_default = enum_default[0] + + if enum_default in self.valid_keys: + self.value_on_not_set = enum_default + + else: + for key in all_keys: + if self.value_on_not_set is NOT_SET: + self.value_on_not_set = key + break self.valid_value_types = (STRING_TYPE, ) From 39976e8bc4b47e778e87e4cef2b7f4d223bbc801 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 10 Aug 2021 17:53:04 +0200 Subject: [PATCH 320/333] conditional dict may have defined default item for enum --- openpype/settings/entities/dict_conditional.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d275d8ac3d..b7c64f173f 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -141,6 +141,7 @@ class DictConditionalEntity(ItemEntity): self.enum_key = self.schema_data.get("enum_key") self.enum_label = self.schema_data.get("enum_label") self.enum_children = self.schema_data.get("enum_children") + self.enum_default = self.schema_data.get("enum_default") self.enum_entity = None @@ -277,15 +278,22 @@ class DictConditionalEntity(ItemEntity): if isinstance(item, dict) and "key" in item: valid_enum_items.append(item) + enum_keys = [] enum_items = [] for item in valid_enum_items: item_key = item["key"] + enum_keys.append(item_key) item_label = item.get("label") or item_key enum_items.append({item_key: item_label}) if not enum_items: return + if self.enum_default in enum_keys: + default_key = self.enum_default + else: + default_key = enum_keys[0] + # Create Enum child first enum_key = self.enum_key or "invalid" enum_schema = { @@ -293,7 +301,8 @@ class DictConditionalEntity(ItemEntity): "multiselection": False, "enum_items": enum_items, "key": enum_key, - "label": self.enum_label + "label": self.enum_label, + "default": default_key } enum_entity = self.create_schema_object(enum_schema, self) From c708388c2084fa11d32ae47db46512a0a4649a90 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 10 Aug 2021 17:54:04 +0200 Subject: [PATCH 321/333] add support and documentation from maya scene patching --- .../plugins/publish/submit_maya_deadline.py | 158 ++++++++++++------ .../defaults/project_settings/deadline.json | 3 +- .../schema_project_deadline.json | 27 ++- website/docs/admin_hosts_maya.md | 20 +++ ...maya-admin_submit_maya_job_to_deadline.png | Bin 0 -> 28550 bytes 5 files changed, 152 insertions(+), 56 deletions(-) create mode 100644 website/docs/assets/maya-admin_submit_maya_job_to_deadline.png diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a652da7786..b607d472bd 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -199,7 +199,7 @@ def get_renderer_variables(renderlayer, root): if extension is None: extension = "png" - if extension == "exr (multichannel)" or extension == "exr (deep)": + if extension in ["exr (multichannel)", "exr (deep)"]: extension = "exr" prefix_attr = "vraySettings.fileNamePrefix" @@ -295,57 +295,70 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): instance.data["toBeRenderedOn"] = "deadline" filepath = None + patches = ( + context.data["project_settings"].get( + "deadline", {}).get( + "publish", {}).get( + "MayaSubmitDeadline", {}).get( + "scene_patches", {}) + ) # Handle render/export from published scene or not ------------------ if self.use_published: + patched_files = [] for i in context: - if "workfile" in i.data["families"]: - assert i.data["publish"] is True, ( - "Workfile (scene) must be published along") - template_data = i.data.get("anatomyData") - rep = i.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - anatomy_filled = anatomy.format(template_data) - template_filled = anatomy_filled["publish"]["path"] - filepath = os.path.normpath(template_filled) - self.log.info("Using published scene for render {}".format( - filepath)) + if "workfile" not in i.data["families"]: + continue + assert i.data["publish"] is True, ( + "Workfile (scene) must be published along") + template_data = i.data.get("anatomyData") + rep = i.data.get("representations")[0].get("name") + template_data["representation"] = rep + template_data["ext"] = rep + template_data["comment"] = None + anatomy_filled = anatomy.format(template_data) + template_filled = anatomy_filled["publish"]["path"] + filepath = os.path.normpath(template_filled) + self.log.info("Using published scene for render {}".format( + filepath)) - if not os.path.exists(filepath): - self.log.error("published scene does not exist!") - raise - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - new_scene = os.path.splitext( - os.path.basename(filepath))[0] - orig_scene = os.path.splitext( - os.path.basename(context.data["currentFile"]))[0] - exp = instance.data.get("expectedFiles") + if not os.path.exists(filepath): + self.log.error("published scene does not exist!") + raise + # now we need to switch scene in expected files + # because token will now point to published + # scene file and that might differ from current one + new_scene = os.path.splitext( + os.path.basename(filepath))[0] + orig_scene = os.path.splitext( + os.path.basename(context.data["currentFile"]))[0] + exp = instance.data.get("expectedFiles") - if isinstance(exp[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in exp[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - f.replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in exp: - new_exp.append( + if isinstance(exp[0], dict): + # we have aovs and we need to iterate over them + new_exp = {} + for aov, files in exp[0].items(): + replaced_files = [] + for f in files: + replaced_files.append( f.replace(orig_scene, new_scene) ) - instance.data["expectedFiles"] = [new_exp] - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) + new_exp[aov] = replaced_files + instance.data["expectedFiles"] = [new_exp] + else: + new_exp = [] + for f in exp: + new_exp.append( + f.replace(orig_scene, new_scene) + ) + instance.data["expectedFiles"] = [new_exp] + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) + # patch workfile is needed + if filepath not in patched_files: + patched_file = self._patch_workfile(filepath, patches) + patched_files.append(patched_file) all_instances = [] for result in context.data["results"]: @@ -868,10 +881,11 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload["JobInfo"].update(job_info_ext) payload["PluginInfo"].update(plugin_info_ext) - envs = [] - for k, v in payload["JobInfo"].items(): - if k.startswith("EnvironmentKeyValue"): - envs.append(v) + envs = [ + v + for k, v in payload["JobInfo"].items() + if k.startswith("EnvironmentKeyValue") + ] # add app name to environment envs.append( @@ -892,11 +906,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): envs.append( "OPENPYPE_ASS_EXPORT_STEP={}".format(1)) - i = 0 - for e in envs: + for i, e in enumerate(envs): payload["JobInfo"]["EnvironmentKeyValue{}".format(i)] = e - i += 1 - return payload def _get_vray_render_payload(self, data): @@ -1003,7 +1014,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): """ if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa + kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) # add 10sec timeout before bailing out kwargs['timeout'] = 10 return requests.post(*args, **kwargs) @@ -1022,7 +1033,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): """ if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa + kwargs['verify'] = not os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) # add 10sec timeout before bailing out kwargs['timeout'] = 10 return requests.get(*args, **kwargs) @@ -1069,3 +1080,42 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): result = filename_zero.replace("\\", "/") return result + + def _patch_workfile(self, file, patches): + # type: (str, dict) -> Union[str, None] + """Patch Maya scene. + + This will take list of patches (lines to add) and apply them to + *published* Maya scene file (that is used later for rendering). + + Patches are dict with following structure:: + { + "name": "Name of patch", + "regex": "regex of line before patch", + "line": "line to insert" + } + + Args: + file (str): File to patch. + patches (dict): Dictionary defining patches. + + Returns: + str: Patched file path or None + + """ + if os.path.splitext(file)[1].lower() != ".ma" or not patches: + return None + + compiled_regex = [re.compile(p["regex"]) for p in patches] + with open(file, "r+") as pf: + scene_data = pf.readlines() + for ln, line in enumerate(scene_data): + for i, r in enumerate(compiled_regex): + if re.match(r, line): + scene_data.insert(ln + 1, patches[i]["line"]) + pf.seek(0) + pf.writelines(scene_data) + pf.truncate() + self.log.info( + "Applied {} patch to scene.".format(patches[i]["name"])) + return file diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 0f2da9f5b0..efeafbb1ac 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -45,7 +45,8 @@ "group": "none", "limit": [], "jobInfo": {}, - "pluginInfo": {} + "pluginInfo": {}, + "scene_patches": [] }, "NukeSubmitDeadline": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 8e6a4b10e4..53c6bf48c0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -151,7 +151,7 @@ "type": "dict", "collapsible": true, "key": "MayaSubmitDeadline", - "label": "Submit maya job to deadline", + "label": "Submit Maya job to Deadline", "checkbox_key": "enabled", "children": [ { @@ -213,6 +213,31 @@ "type": "raw-json", "key": "pluginInfo", "label": "Additional PluginInfo data" + }, + { + "type": "list", + "key": "scene_patches", + "label": "Scene patches", + "required_keys": ["name", "regex", "line"], + "object_type": { + "type": "dict", + "children": [ + { + "key": "name", + "label": "Patch name", + "type": "text" + }, { + "key": "regex", + "label": "Patch regex", + "type": "text" + }, { + "key": "line", + "label": "Patch line", + "type": "text" + } + ] + + } } ] }, diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 5e0aa15345..47447983b9 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -87,6 +87,26 @@ When you publish your model with top group named like `foo_GRP` it will fail. Bu All regexes used here are in Python variant. ::: +### Maya > Deadline submitter +This plugin provides connection between Maya and Deadline. It is using [Deadline Webservice](https://docs.thinkboxsoftware.com/products/deadline/10.0/1_User%20Manual/manual/web-service.html) to submit jobs to farm. +![Maya > Deadline Settings](assets/maya-admin_submit_maya_job_to_deadline.png) + +You can set various aspects of scene submission to farm with per-project settings in **Setting UI**. + + - **Optional** will mark sumission plugin optional + - **Active** will enable/disable plugin + - **Tile Assembler Plugin** will set what should be used to assemble tiles on Deadline. Either **Open Image IO** will be used +or Deadlines **Draft Tile Assembler**. + - **Use Published scene** enable to render from published scene instead of scene in work area. Rendering from published files is much safer. + - **Use Asset dependencies** will mark job pending on farm until asset dependencies are fulfilled - for example Deadline will wait for scene file to be synced to cloud, etc. + - **Group name** use specific Deadline group for the job. + - **Limit Groups** use these Deadline Limit groups for the job. + - **Additional `JobInfo` data** JSON of additional Deadline options that will be embedded in `JobInfo` part of the submission data. + - **Additional `PluginInfo` data** JSON of additional Deadline options that will be embedded in `PluginInfo` part of the submission data. + - **Scene patches** - configure mechanism to add additional lines to published Maya Ascii scene files before they are used for rendering. +This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation. +`Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending. + ## Custom Menu You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**. ![Custom menu definition](assets/maya-admin_scriptsmenu.png) diff --git a/website/docs/assets/maya-admin_submit_maya_job_to_deadline.png b/website/docs/assets/maya-admin_submit_maya_job_to_deadline.png new file mode 100644 index 0000000000000000000000000000000000000000..56b720dc5deea7e891ed8747edac7bdd1f2f041e GIT binary patch literal 28550 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 Date: Wed, 11 Aug 2021 03:41:59 +0000 Subject: [PATCH 322/333] [Automated] Bump version --- CHANGELOG.md | 19 ++++++++----------- openpype/version.py | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 964120330e..0f2cb2b1ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [3.3.0-nightly.8](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.9](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) **🚀 Enhancements** +- submodules: avalon-core update [\#1911](https://github.com/pypeclub/OpenPype/pull/1911) - Feature AE local render [\#1901](https://github.com/pypeclub/OpenPype/pull/1901) - Ftrack: Where I run action enhancement [\#1900](https://github.com/pypeclub/OpenPype/pull/1900) - Ftrack: Private project server actions [\#1899](https://github.com/pypeclub/OpenPype/pull/1899) @@ -29,10 +30,16 @@ - nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829) - Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823) - Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) +- Settings list can use template or schema as object type [\#1815](https://github.com/pypeclub/OpenPype/pull/1815) +- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) - Maya: Shader name validation [\#1762](https://github.com/pypeclub/OpenPype/pull/1762) **🐛 Bug fixes** +- Nuke: update video file crassing [\#1916](https://github.com/pypeclub/OpenPype/pull/1916) +- Fix - texture validators for workfiles triggers only for textures workfiles [\#1914](https://github.com/pypeclub/OpenPype/pull/1914) +- Settings UI: List order works as expected [\#1906](https://github.com/pypeclub/OpenPype/pull/1906) +- Hiero: loaded clip was not set colorspace from version data [\#1904](https://github.com/pypeclub/OpenPype/pull/1904) - Pyblish UI: Fix collecting stage processing [\#1903](https://github.com/pypeclub/OpenPype/pull/1903) - Burnins: Use input's bitrate in h624 [\#1902](https://github.com/pypeclub/OpenPype/pull/1902) - Bug: fixed python detection [\#1893](https://github.com/pypeclub/OpenPype/pull/1893) @@ -48,7 +55,6 @@ - Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) - Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Failsafe for cross project containers. [\#1806](https://github.com/pypeclub/OpenPype/pull/1806) -- Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) - Settings error dialog on show [\#1798](https://github.com/pypeclub/OpenPype/pull/1798) **Merged pull requests:** @@ -65,7 +71,6 @@ - Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) - Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799) -- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) - Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) - Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) - Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776) @@ -74,7 +79,6 @@ - Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) - Deadline: Nuke submission additional attributes [\#1756](https://github.com/pypeclub/OpenPype/pull/1756) -- Settings schema without prefill [\#1753](https://github.com/pypeclub/OpenPype/pull/1753) **🐛 Bug fixes** @@ -90,24 +94,17 @@ - Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764) - Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761) - Anatomy others templates don't cause crash [\#1758](https://github.com/pypeclub/OpenPype/pull/1758) -- Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745) **Merged pull requests:** - Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808) - Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) - Bc/fix/docs [\#1771](https://github.com/pypeclub/OpenPype/pull/1771) -- TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4) -**Merged pull requests:** - -- celaction fixes [\#1754](https://github.com/pypeclub/OpenPype/pull/1754) -- celaciton: audio subset changed data structure [\#1750](https://github.com/pypeclub/OpenPype/pull/1750) - ## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) diff --git a/openpype/version.py b/openpype/version.py index c888e5f9d9..c4bd5a14cb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.8" +__version__ = "3.3.0-nightly.9" From da65b2e36a958ddd5edca66f6f57481934563b66 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 10:33:15 +0200 Subject: [PATCH 323/333] fix removing of duplicated keys --- .../tools/settings/settings/dict_mutable_widget.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/dict_mutable_widget.py b/openpype/tools/settings/settings/dict_mutable_widget.py index 4f2800156c..3526dc60b5 100644 --- a/openpype/tools/settings/settings/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/dict_mutable_widget.py @@ -792,10 +792,6 @@ class DictMutableKeysWidget(BaseWidget): def remove_key(self, widget): key = self.entity.get_child_key(widget.entity) self.entity.pop(key) - # Poping of key from entity should remove the entity and input field. - # this is kept for testing purposes. - if widget in self.input_fields: - self.remove_row(widget) def change_key(self, new_key, widget): if not new_key or widget.is_key_duplicated: @@ -862,6 +858,11 @@ class DictMutableKeysWidget(BaseWidget): return input_field def remove_row(self, widget): + if widget.is_key_duplicated: + new_key = widget.uuid_key + if new_key is None: + new_key = str(uuid4()) + self.validate_key_duplication(widget.temp_key, new_key, widget) self.input_fields.remove(widget) self.content_layout.removeWidget(widget) widget.deleteLater() @@ -945,7 +946,10 @@ class DictMutableKeysWidget(BaseWidget): _input_field.set_entity_value() else: - if input_field.key_value() != key: + if ( + not input_field.is_key_duplicated + and input_field.key_value() != key + ): changed = True input_field.set_key(key) From 0503a3a9ff670d5cb721a4b8dee7d9c95df93a9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 10:51:38 +0200 Subject: [PATCH 324/333] added default to readme --- openpype/settings/entities/schemas/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 42a8973f43..ff19a439d9 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -195,6 +195,7 @@ - all items in `enum_children` must have at least `key` key which represents value stored under `enum_key` - items can define `label` for UI purposes - most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`) +- to set default value for `enum_key` set it with `enum_default` - entity must have defined `"label"` if is not used as widget - is set as group if any parent is not group - if `"label"` is entetered there which will be shown in GUI @@ -359,6 +360,8 @@ How output of the schema could look like on save: - values are defined under value of key `"enum_items"` as list - each item in list is simple dictionary where value is label and key is value which will be stored - should be possible to enter single dictionary if order of items doesn't matter +- it is possible to set default selected value/s with `default` attribute + - it is recommended to use this option only in single selection mode ``` { From 3001f4c3ccdd980559ccf5ff1aea6bef7a1fd5cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 10:53:25 +0200 Subject: [PATCH 325/333] added one more comment line --- openpype/settings/entities/schemas/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index ff19a439d9..b437340f5b 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -362,6 +362,7 @@ How output of the schema could look like on save: - should be possible to enter single dictionary if order of items doesn't matter - it is possible to set default selected value/s with `default` attribute - it is recommended to use this option only in single selection mode + - at the end this option is used only when defying default settings value or in dynamic items ``` { From 2f28514c1818983342755962d164a5d905b54e6d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 10:56:03 +0200 Subject: [PATCH 326/333] fixed typo --- openpype/settings/entities/schemas/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index b437340f5b..2034d4e463 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -375,7 +375,7 @@ How output of the schema could look like on save: {"ftrackreview": "Add to Ftrack"}, {"delete": "Delete output"}, {"slate-frame": "Add slate frame"}, - {"no-hnadles": "Skip handle frames"} + {"no-handles": "Skip handle frames"} ] } ``` From 32630ca703a74881ed7856cef4c3afc3ac82f998 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 11 Aug 2021 12:03:00 +0200 Subject: [PATCH 327/333] Fix - validate takes repre["files"] as list all the time repre["files"] might be list or str, loop for string wasn't working --- .../plugins/publish/validate_expected_and_rendered_files.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index c71b5106ec..305c71b035 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -181,6 +181,10 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): """Returns set of file names from metadata.json""" expected_files = set() - for file_name in repre["files"]: + files = repre["files"] + if not isinstance(files, list): + files = [files] + + for file_name in files: expected_files.add(file_name) return expected_files From 112aac785d3e538b35a14d389b549ddb7ae22107 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Aug 2021 12:47:51 +0200 Subject: [PATCH 328/333] =?UTF-8?q?fix=20hound=20=F0=9F=90=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index b607d472bd..6b52e4b387 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -1082,7 +1082,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): return result def _patch_workfile(self, file, patches): - # type: (str, dict) -> Union[str, None] + # type: (str, dict) -> [str, None] """Patch Maya scene. This will take list of patches (lines to add) and apply them to @@ -1117,5 +1117,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): pf.writelines(scene_data) pf.truncate() self.log.info( - "Applied {} patch to scene.".format(patches[i]["name"])) + "Applied {} patch to scene.".format( + patches[i]["name"])) return file From 62cea5a26b59b81b382885f1ffaf7c7309416bd4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 11 Aug 2021 14:39:18 +0200 Subject: [PATCH 329/333] check for missing python when using pyenv --- README.md | 13 +++++++++++-- tools/create_env.ps1 | 10 ++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b4495c9b6..209af24c75 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The main things you will need to run and build OpenPype are: - PowerShell 5.0+ (Windows) - Bash (Linux) - [**Python 3.7.8**](#python) or higher -- [**MongoDB**](#database) +- [**MongoDB**](#database) (needed only for local development) It can be built and ran on all common platforms. We develop and test on the following: @@ -126,6 +126,16 @@ pyenv local 3.7.9 ### Linux +#### Docker +Easiest way to build OpenPype on Linux is using [Docker](https://www.docker.com/). Just run: + +```sh +sudo ./tools/docker_build.sh +``` + +If all is successful, you'll find built OpenPype in `./build/` folder. + +#### Manual build You will need [Python 3.7](https://www.python.org/downloads/) and [git](https://git-scm.com/downloads). You'll also need [curl](https://curl.se) on systems that doesn't have one preinstalled. To build Python related stuff, you need Python header files installed (`python3-dev` on Ubuntu for example). @@ -133,7 +143,6 @@ To build Python related stuff, you need Python header files installed (`python3- You'll need also other tools to build some OpenPype dependencies like [CMake](https://cmake.org/). Python 3 should be part of all modern distributions. You can use your package manager to install **git** and **cmake**. -
Details for Ubuntu Install git, cmake and curl diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index e2ec401bb3..f19a98f11b 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -50,8 +50,18 @@ function Install-Poetry() { Write-Host "Installing Poetry ... " $python = "python" if (Get-Command "pyenv" -ErrorAction SilentlyContinue) { + if (-not (Test-Path -PathType Leaf -Path "$($openpype_root)\.python-version")) { + $result = & pyenv global + if ($result -eq "no global version configured") { + Write-Host "!!! " -NoNewline -ForegroundColor Red + Write-Host "Using pyenv but having no local or global version of Python set." + Exit-WithCode 1 + } + } $python = & pyenv which python + } + $env:POETRY_HOME="$openpype_root\.poetry" (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | & $($python) - } From 2199d8bed52d3ca6e298c20b60e1cc1dc6ba5bf3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 11 Aug 2021 14:53:54 +0200 Subject: [PATCH 330/333] Nuke: submit to farm failed due `ftrack` family remove --- openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py | 1 - openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py index 5611591b56..b0d3ec6241 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py @@ -51,7 +51,6 @@ class ExtractReviewDataLut(openpype.api.Extractor): if "render.farm" in families: instance.data["families"].remove("review") - instance.data["families"].remove("ftrack") self.log.debug( "_ lutPath: {}".format(instance.data["lutPath"])) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 5032e602a2..cea7d86c26 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -45,7 +45,6 @@ class ExtractReviewDataMov(openpype.api.Extractor): if "render.farm" in families: instance.data["families"].remove("review") - instance.data["families"].remove("ftrack") data = exporter.generate_mov(farm=True) self.log.debug( From 9a4fba11c430da6370c24e8c84ced78dca1bded2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 16:12:48 +0200 Subject: [PATCH 331/333] replaced svg logos with newer version without "text" nodes --- website/static/img/logos/pypeclub_black.svg | 20 +++++++++++--- .../static/img/logos/pypeclub_color_white.svg | 26 ++++++++++++++----- website/static/img/logos/pypeclub_white.svg | 20 +++++++++++--- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/website/static/img/logos/pypeclub_black.svg b/website/static/img/logos/pypeclub_black.svg index b749edbdb3..6c209977fe 100644 --- a/website/static/img/logos/pypeclub_black.svg +++ b/website/static/img/logos/pypeclub_black.svg @@ -1,8 +1,8 @@ - - + + @@ -20,7 +20,21 @@ - .club + + + + + + + + + + + + + + + diff --git a/website/static/img/logos/pypeclub_color_white.svg b/website/static/img/logos/pypeclub_color_white.svg index c82946d82b..ffa194aa47 100644 --- a/website/static/img/logos/pypeclub_color_white.svg +++ b/website/static/img/logos/pypeclub_color_white.svg @@ -1,26 +1,40 @@ - - + + - + - + - + - .club + + + + + + + + + + + + + + + diff --git a/website/static/img/logos/pypeclub_white.svg b/website/static/img/logos/pypeclub_white.svg index b634c210b1..3bf4159f9c 100644 --- a/website/static/img/logos/pypeclub_white.svg +++ b/website/static/img/logos/pypeclub_white.svg @@ -1,8 +1,8 @@ - - + + @@ -20,7 +20,21 @@ - .club + + + + + + + + + + + + + + + From 1e5bf01ea8ea0eb090263157e86accc4c4a7d984 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 11 Aug 2021 16:13:06 +0200 Subject: [PATCH 332/333] saved new defaults for unreal --- openpype/settings/defaults/project_settings/unreal.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 46b9ca2a18..dad61cd1f0 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -1,6 +1,5 @@ { "project_setup": { - "dev_mode": true, - "install_unreal_python_engine": false + "dev_mode": true } } \ No newline at end of file From 6479e09ecbb660bc7c7afd44d360a2a7ac780812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 12 Aug 2021 14:49:47 +0200 Subject: [PATCH 333/333] repair accident deletion --- openpype/settings/defaults/project_settings/maya.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index dc95632bb8..592b424fd8 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -177,12 +177,14 @@ }, "ValidateModelName": { "enabled": false, + "database": true, "material_file": { "windows": "", "darwin": "", "linux": "" }, - "regex": "(.*)_(\\\\d)*_(.*)_(GEO)" + "regex": "(.*)_(\\d)*_(?P.*)_(GEO)", + "top_level_regex": ".*_GRP" }, "ValidateTransformNamingSuffix": { "enabled": true,