From b29362411e2576fb8d871e2d750641ed491a11f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 14:32:12 +0100 Subject: [PATCH 01/54] add on/off to env value to bool --- pype/lib/env_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/lib/env_tools.py b/pype/lib/env_tools.py index f31426103b..a5176b814d 100644 --- a/pype/lib/env_tools.py +++ b/pype/lib/env_tools.py @@ -20,9 +20,9 @@ def env_value_to_bool(env_key=None, value=None, default=False): if value is not None: value = str(value).lower() - if value in ("true", "yes", "1"): + if value in ("true", "yes", "1", "on"): return True - elif value in ("false", "no", "0"): + elif value in ("false", "no", "0", "off"): return False return default From 2164dbe6c18a7d9dccfaec79905e1232103dbf14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 14:33:08 +0100 Subject: [PATCH 02/54] added to lib python functions for work with python modules and classes --- pype/lib/__init__.py | 10 ++++ pype/lib/python_module_tools.py | 101 ++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 pype/lib/python_module_tools.py diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 188dd68039..426a5802c3 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -11,6 +11,12 @@ from .env_tools import ( get_paths_from_environ ) +from .python_module_tools import ( + modules_from_path, + recursive_bases_from_class, + classes_from_module +) + from .avalon_context import ( is_latest, any_outdated, @@ -53,6 +59,10 @@ __all__ = [ "env_value_to_bool", "get_paths_from_environ", + "modules_from_path", + "recursive_bases_from_class", + "classes_from_module", + "is_latest", "any_outdated", "get_asset", diff --git a/pype/lib/python_module_tools.py b/pype/lib/python_module_tools.py new file mode 100644 index 0000000000..61a3b1b09e --- /dev/null +++ b/pype/lib/python_module_tools.py @@ -0,0 +1,101 @@ +import os +import types +import inspect +import logging + +log = logging.getLogger(__name__) + + +def modules_from_path(folder_path): + """Get python scripts as modules from a path. + + Arguments: + path (str): Path to folder containing python scripts. + + Returns: + List of modules. + """ + + folder_path = os.path.normpath(folder_path) + + modules = [] + if not os.path.isdir(folder_path): + log.warning("Not a directory path: {}".format(folder_path)) + return modules + + for filename in os.listdir(folder_path): + # Ignore files which start with underscore + if filename.startswith("_"): + continue + + mod_name, mod_ext = os.path.splitext(filename) + if not mod_ext == ".py": + continue + + full_path = os.path.join(folder_path, filename) + if not os.path.isfile(full_path): + continue + + try: + # Prepare module object where content of file will be parsed + module = types.ModuleType(mod_name) + module.__file__ = full_path + + with open(full_path) as _stream: + # Execute content and store it to module object + exec(_stream.read(), module.__dict__) + + modules.append(module) + + except Exception: + log.warning( + "Failed to load path: \"{0}\"".format(full_path), + exc_info=True + ) + continue + + return modules + + +def recursive_bases_from_class(klass): + """Extract all bases from entered class.""" + result = [] + bases = klass.__bases__ + result.extend(bases) + for base in bases: + result.extend(recursive_bases_from_class(base)) + return result + + +def classes_from_module(superclass, module): + """Return plug-ins from module + + Arguments: + superclass (superclass): Superclass of subclasses to look for + module (types.ModuleType): Imported module from which to + parse valid Avalon plug-ins. + + Returns: + List of plug-ins, or empty list if none is found. + + """ + + classes = list() + for name in dir(module): + # It could be anything at this point + obj = getattr(module, name) + if not inspect.isclass(obj): + continue + + # These are subclassed from nothing, not even `object` + if not len(obj.__bases__) > 0: + continue + + # Use string comparison rather than `issubclass` + # in order to support reloading of this module. + bases = recursive_bases_from_class(obj) + if not any(base.__name__ == superclass.__name__ for base in bases): + continue + + classes.append(obj) + return classes From a3afd0b5cffe739136dec376aa574cc8846450e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 14:33:56 +0100 Subject: [PATCH 03/54] applications has new class LaunchHook similar to PypeHook --- pype/lib/applications.py | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 600530a00f..8653e77da1 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -713,6 +713,74 @@ class Application: return self.manager.launch(self.app_name, *args, **kwargs) +@six.add_metaclass(ABCMeta) +class LaunchHook: + """Abstract class from all hooks should inherit.""" + # Order of prelaunch hook, will be executed as last if set to None. + order = None + # If hook should be executed befor or after application launch + prelaunch = True + # List of host implementations, skipped if empty. + hosts = [] + # List of platform availability, skipped if empty. + platforms = [] + + def __init__(self, launch_context): + """Constructor of launch hook. + + Always should be called + """ + self.log = logging.getLogger(self.__class__.__name__) + + self.launch_context = launch_context + + is_valid = self.class_validation(launch_context) + if is_valid: + is_valid = self.validate() + + self.is_valid = is_valid + + @classmethod + def class_validation(cls, launch_context): + """Validation of class attributes by launch context. + + Args: + launch_context (ApplicationLaunchContext): Context of launching + application. + + Returns: + bool: Is launch hook valid for the context by class attributes. + """ + if cls.platforms: + low_platforms = tuple( + _platform.lower() + for _platform in cls.platforms + ) + if platform.system().lower() not in low_platforms: + return False + + if cls.hosts: + if launch_context.host_name not in cls.hosts: + return False + + return True + + def validate(self): + """Optional validation of launch hook on initialization. + + Returns: + bool: Hook is valid (True) or invalid (False). + """ + # QUESTION Not sure if this method has any usable potential. + # - maybe result can be based on settings + return True + + @abstractmethod + def execute(self, *args, **kwargs): + """Abstract execute method where logic of hook is.""" + pass + + class ApplicationLaunchContext: """Context of launching application. From 52ff856bb65824c280a29fb5c4351f652f6142a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 14:34:30 +0100 Subject: [PATCH 04/54] application context has method to load launch hooks --- pype/lib/applications.py | 75 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 8653e77da1..0a6f7b481f 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -5,9 +5,12 @@ import getpass import json import copy import platform +import inspect import logging import subprocess +from abc import ABCMeta, abstractmethod +import six import acre import avalon.lib @@ -853,6 +856,78 @@ class ApplicationLaunchContext: self.prepare_host_environments() self.prepare_context_environments() + def paths_to_launch_hook(self): + """Directory paths where to look for launch hooks.""" + # This method has potential to be part of application manager (maybe). + paths = [] + + return paths + + def discover_launch_hooks(self): + classes = [] + paths = self.paths_to_launch_hook() + for path in paths: + if not os.path.exists(path): + self.log.info( + "Path to launch hooks does not exists: \"{}\"".format(path) + ) + continue + + modules = modules_from_path(path) + for _module in modules: + classes.extend(classes_from_module(LaunchHook, _module)) + + pre_hooks_with_order = [] + pre_hooks_without_order = [] + post_hooks_with_order = [] + post_hooks_without_order = [] + for klass in classes: + try: + hook = klass(self) + if not hook.is_valid: + self.log.debug( + "Hook is not valid for curent launch context." + ) + continue + + if inspect.isabstract(hook): + self.log.debug("Skipped abstract hook: {}".format( + str(hook) + )) + continue + + # Separate hooks if should be executed before or after launch + if hook.prelaunch: + if hook.order is None: + pre_hooks_without_order.append(hook) + else: + pre_hooks_with_order.append(hook) + else: + if hook.order is None: + post_hooks_with_order.append(hook) + else: + post_hooks_without_order.append(hook) + + except Exception: + self.log.warning( + "Initialization of hook failed. {}".format(str(klass)), + exc_info=True + ) + + # Sort hooks with order by order + pre_hooks_ordered = list(sorted( + pre_hooks_with_order, key=lambda obj: obj.order + )) + post_hooks_ordered = list(sorted( + post_hooks_with_order, key=lambda obj: obj.order + )) + + # Extend ordered hooks with hooks without defined order + pre_hooks_ordered.extend(pre_hooks_without_order) + post_hooks_ordered.extend(post_hooks_without_order) + + return pre_hooks_ordered, post_hooks_ordered + @property def app_name(self): return self.application.app_name From 17b05915870d8673ed228d2782e48d5b8879b992 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 14:43:28 +0100 Subject: [PATCH 05/54] added base of getting hooks dir paths --- pype/lib/applications.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 0a6f7b481f..b17b1fdbb0 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -23,6 +23,10 @@ from ..api import ( system_settings, environments ) +from .python_module_tools import ( + modules_from_path, + classes_from_module +) from .hooks import execute_hook from .deprecated import get_avalon_database from .env_tools import env_value_to_bool @@ -856,16 +860,28 @@ class ApplicationLaunchContext: self.prepare_host_environments() self.prepare_context_environments() - def paths_to_launch_hook(self): + def paths_to_launch_hooks(self): """Directory paths where to look for launch hooks.""" # This method has potential to be part of application manager (maybe). + + # TODO find better way how to define dir path to default launch hooks + import pype + pype_dir = os.path.dirname(os.path.abspath(pype.__file__)) + hooks_dir = os.path.join(pype_dir, "hooks") + + # TODO load additional studio paths from settings + # TODO add paths based on used modules (like `ftrack`) paths = [] - + subfolder_names = ["global", self.host_name, self.app_name] + for subfolder_name in subfolder_names: + path = os.path.join(hooks_dir, subfolder_name) + if os.path.exists(path) and os.path.isdir(path): + paths.append(path) return paths def discover_launch_hooks(self): classes = [] - paths = self.paths_to_launch_hook() + paths = self.paths_to_launch_hooks() for path in paths: if not os.path.exists(path): self.log.info( From e545a6798aa513818a370bb97d86d29a1fd0d63b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 14:46:22 +0100 Subject: [PATCH 06/54] prelaunch and post launch hooks are discovered and executed --- pype/lib/applications.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index b17b1fdbb0..b135db8057 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -968,20 +968,31 @@ class ApplicationLaunchContext: self.log.warning("Application was already launched.") return + # Discover launch hooks + prelaunch_hooks, postlaunch_hooks = self.discover_launch_hooks() + + # Execute prelaunch hooks + for prelaunch_hook in prelaunch_hooks: + prelaunch_hook.execute() + + # Prepare subprocess args args = self.clear_launch_args(self.launch_args) self.log.debug( "Launching \"{}\" with args: {}".format(self.app_name, args) ) + # Run process self.process = subprocess.Popen(args, **self.kwargs) - # TODO do this with after-launch hooks - try: - self.after_launch_procedures() - except Exception: - self.log.warning( - "After launch procedures were not successful.", - exc_info=True - ) + # Process post launch hooks + for postlaunch_hook in postlaunch_hooks: + try: + postlaunch_hook.execute() + + except Exception: + self.log.warning( + "After launch procedures were not successful.", + exc_info=True + ) return self.process From 0dc51f3d6d7fe05823c17eb0dfdef5f8cdd43648 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 14:51:32 +0100 Subject: [PATCH 07/54] hooks are launch hooks are stored to launch context --- pype/lib/applications.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index b135db8057..5aca9ac90b 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -853,6 +853,9 @@ class ApplicationLaunchContext: ) self.kwargs["creationflags"] = flags + self.prelaunch_hooks = None + self.postlaunch_hooks = None + self.process = None # TODO move these to pre-paunch hook @@ -879,7 +882,18 @@ class ApplicationLaunchContext: paths.append(path) return paths - def discover_launch_hooks(self): + def discover_launch_hooks(self, force=False): + if ( + self.prelaunch_hooks is not None + or self.postlaunch_hooks is not None + ): + if not force: + self.log.info("Launch hooks were already discovered.") + return + + self.prelaunch_hooks.clear() + self.postlaunch_hooks.clear() + classes = [] paths = self.paths_to_launch_hooks() for path in paths: @@ -942,7 +956,8 @@ class ApplicationLaunchContext: pre_hooks_ordered.extend(pre_hooks_without_order) post_hooks_ordered.extend(post_hooks_without_order) - return pre_hooks_ordered, post_hooks_ordered + self.prelaunch_hooks = pre_hooks_ordered + self.postlaunch_hooks = post_hooks_ordered @property def app_name(self): @@ -969,10 +984,10 @@ class ApplicationLaunchContext: return # Discover launch hooks - prelaunch_hooks, postlaunch_hooks = self.discover_launch_hooks() + self.discover_launch_hooks() # Execute prelaunch hooks - for prelaunch_hook in prelaunch_hooks: + for prelaunch_hook in self.prelaunch_hooks: prelaunch_hook.execute() # Prepare subprocess args @@ -984,7 +999,7 @@ class ApplicationLaunchContext: self.process = subprocess.Popen(args, **self.kwargs) # Process post launch hooks - for postlaunch_hook in postlaunch_hooks: + for postlaunch_hook in self.postlaunch_hooks: try: postlaunch_hook.execute() From 97c46f79c9134e7942c914e40f40a0d71665a63c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 15:41:19 +0100 Subject: [PATCH 08/54] few small changes --- pype/lib/__init__.py | 2 ++ pype/lib/applications.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 426a5802c3..512cb12e37 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -34,6 +34,7 @@ from .applications import ( ApplictionExecutableNotFound, ApplicationNotFound, ApplicationManager, + LaunchHook, launch_application, ApplicationAction, _subprocess @@ -78,6 +79,7 @@ __all__ = [ "ApplictionExecutableNotFound", "ApplicationNotFound", "ApplicationManager", + "LaunchHook", "launch_application", "ApplicationAction", diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 5aca9ac90b..ca80676cba 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -883,6 +883,7 @@ class ApplicationLaunchContext: return paths def discover_launch_hooks(self, force=False): + """Load and prepare launch hooks.""" if ( self.prelaunch_hooks is not None or self.postlaunch_hooks is not None @@ -1000,6 +1001,8 @@ class ApplicationLaunchContext: # Process post launch hooks for postlaunch_hook in self.postlaunch_hooks: + # TODO how to handle errors? + # - store to variable to let them accesible? try: postlaunch_hook.execute() From 687ccf7217cea94fae380af0453ededb7afb7887 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 17:05:20 +0100 Subject: [PATCH 09/54] env attribute directly points to kwargs --- pype/lib/applications.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index ca80676cba..ed21a1b7e5 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -824,14 +824,6 @@ class ApplicationLaunchContext: self.data = dict(data) - # Handle launch environemtns - passed_env = self.data.pop("env", None) - if passed_env is None: - env = os.environ - else: - env = passed_env - self.env = copy.deepcopy(env) - # Load settings if were not passed in data settings_env = self.data.get("settings_env") if settings_env is None: @@ -840,9 +832,17 @@ class ApplicationLaunchContext: # subprocess.Popen launch arguments (first argument in constructor) self.launch_args = [executable] + + # Handle launch environemtns + passed_env = self.data.pop("env", None) + if passed_env is None: + env = os.environ + else: + env = passed_env + # subprocess.Popen keyword arguments self.kwargs = { - "env": self.env + "env": copy.deepcopy(env) } if platform.system().lower() == "windows": @@ -862,6 +862,24 @@ class ApplicationLaunchContext: self.prepare_global_data() self.prepare_host_environments() self.prepare_context_environments() + @property + def env(self): + if ( + "env" not in self.kwargs + or self.kwargs["env"] is None + ): + self.kwargs["env"] = {} + return self.kwargs["env"] + + @env.setter + def env(self, value): + if not isinstance(value, dict): + raise ValueError( + "'env' attribute expect 'dict' object. Got: {}".format( + str(type(value)) + ) + ) + self.kwargs["env"] = value def paths_to_launch_hooks(self): """Directory paths where to look for launch hooks.""" From 9a814320ba6952025fce0c2215214b2792e33432 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 17:05:46 +0100 Subject: [PATCH 10/54] LaunchHook is using PypeLogger --- pype/lib/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index ed21a1b7e5..80b698ecc8 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -737,7 +737,7 @@ class LaunchHook: Always should be called """ - self.log = logging.getLogger(self.__class__.__name__) + self.log = Logger().get_logger(self.__class__.__name__) self.launch_context = launch_context From baa10c6eb50fca4e5c61993774eed7857348e92b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 17:06:47 +0100 Subject: [PATCH 11/54] LaunchHook has few wrapped attributes --- pype/lib/applications.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 80b698ecc8..675b0c90be 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -772,6 +772,26 @@ class LaunchHook: return True + @property + def data(self): + return self.launch_context.data + + @property + def application(self): + return getattr(self.launch_context, "application", None) + + @property + def manager(self): + return getattr(self.application, "manager", None) + + @property + def host_name(self): + return getattr(self.application, "host_name", None) + + @property + def app_name(self): + return getattr(self.application, "app_name", None) + def validate(self): """Optional validation of launch hook on initialization. From e24ff6725a5ecc11df6d02d7f73e966b6752566b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 17:07:17 +0100 Subject: [PATCH 12/54] created global host launch hook --- pype/hooks/global/global_host_data.py | 366 ++++++++++++++++++++++++++ pype/lib/applications.py | 347 ++---------------------- 2 files changed, 384 insertions(+), 329 deletions(-) create mode 100644 pype/hooks/global/global_host_data.py diff --git a/pype/hooks/global/global_host_data.py b/pype/hooks/global/global_host_data.py new file mode 100644 index 0000000000..3f82f3b099 --- /dev/null +++ b/pype/hooks/global/global_host_data.py @@ -0,0 +1,366 @@ +import os +import re +import json +import getpass +import copy + +from pype.api import ( + Anatomy, + config +) +from pype.lib import ( + env_value_to_bool, + LaunchHook, + ApplicationLaunchFailed +) + +import acre +import avalon.api + + +class GlobalHostDataHook(LaunchHook): + order = -100 + + def execute(self): + """Prepare global objects to `data` that will be used for sure.""" + if not self.application.is_host: + self.log.info( + "Skipped hook {}. Application is not marked as host.".format( + self.__class__.__name__ + ) + ) + return + + self.prepare_global_data() + self.prepare_host_environments() + self.prepare_context_environments() + + def prepare_global_data(self): + """Prepare global objects to `data` that will be used for sure.""" + # Mongo documents + project_name = self.launch_context.data.get("project_name") + if not project_name: + self.log.info( + "Skipping global data preparation." + " Key `project_name` was not found in launch context." + ) + return + + self.log.debug("Project name is set to \"{}\"".format(project_name)) + # Anatomy + self.launch_context.data["anatomy"] = Anatomy(project_name) + + # Mongo connection + dbcon = avalon.api.AvalonMongoDB() + dbcon.Session["AVALON_PROJECT"] = project_name + dbcon.install() + + self.launch_context.data["dbcon"] = dbcon + + # Project document + project_doc = dbcon.find_one({"type": "project"}) + self.launch_context.data["project_doc"] = project_doc + + asset_name = self.launch_context.data.get("asset_name") + if not asset_name: + self.log.warning( + "Asset name was not set. Skipping asset document query." + ) + return + + asset_doc = dbcon.find_one({ + "type": "asset", + "name": asset_name + }) + self.launch_context.data["asset_doc"] = asset_doc + + def _merge_env(self, env, current_env): + """Modified function(merge) from acre module.""" + result = current_env.copy() + for key, value in env.items(): + # Keep missing keys by not filling `missing` kwarg + value = acre.lib.partial_format(value, data=current_env) + result[key] = value + return result + + def prepare_host_environments(self): + """Modify launch environments based on launched app and context.""" + # Keys for getting environments + env_keys = [ + self.launch_context.host_name, + self.launch_context.app_name + ] + + asset_doc = self.launch_context.data.get("asset_doc") + if asset_doc: + # Add tools environments + for key in asset_doc["data"].get("tools_env") or []: + tool = self.manager.tools.get(key) + if tool: + if tool.group_name not in env_keys: + env_keys.append(tool.group_name) + + if tool.name not in env_keys: + env_keys.append(tool.name) + + self.log.debug( + "Finding environment groups for keys: {}".format(env_keys) + ) + + settings_env = self.launch_context.data["settings_env"] + env_values = {} + for env_key in env_keys: + _env_values = settings_env.get(env_key) + if not _env_values: + continue + + # Choose right platform + tool_env = acre.parse(_env_values) + # Merge dictionaries + env_values = self._merge_env(tool_env, env_values) + + final_env = self._merge_env( + acre.compute(env_values), self.launch_context.env + ) + + # Update env + self.launch_context.env.update(final_env) + + def prepare_context_environments(self): + """Modify launch environemnts with context data for launched host.""" + # Context environments + project_doc = self.launch_context.data.get("project_doc") + asset_doc = self.launch_context.data.get("asset_doc") + task_name = self.launch_context.data.get("task_name") + if ( + not project_doc + or not asset_doc + or not task_name + ): + self.log.info( + "Skipping context environments preparation." + " Launch context does not contain required data." + ) + return + + workdir_data = self._prepare_workdir_data( + project_doc, asset_doc, task_name + ) + self.launch_context.data["workdir_data"] = workdir_data + + hierarchy = workdir_data["hierarchy"] + anatomy = self.launch_context.data["anatomy"] + + try: + anatomy_filled = anatomy.format(workdir_data) + workdir = os.path.normpath(anatomy_filled["work"]["folder"]) + if not os.path.exists(workdir): + self.log.debug( + "Creating workdir folder: \"{}\"".format(workdir) + ) + os.makedirs(workdir) + + except Exception as exc: + raise ApplicationLaunchFailed( + "Error in anatomy.format: {}".format(str(exc)) + ) + + context_env = { + "AVALON_PROJECT": project_doc["name"], + "AVALON_ASSET": asset_doc["name"], + "AVALON_TASK": task_name, + "AVALON_APP": self.launch_context.host_name, + "AVALON_APP_NAME": self.launch_context.app_name, + "AVALON_HIERARCHY": hierarchy, + "AVALON_WORKDIR": workdir + } + self.log.debug( + "Context environemnts set:\n{}".format( + json.dumps(context_env, indent=4) + ) + ) + self.launch_context.env.update(context_env) + + self.prepare_last_workfile(workdir) + + def _prepare_workdir_data(self, project_doc, asset_doc, task_name): + hierarchy = "/".join(asset_doc["data"]["parents"]) + + data = { + "project": { + "name": project_doc["name"], + "code": project_doc["data"].get("code") + }, + "task": task_name, + "asset": asset_doc["name"], + "app": self.launch_context.host_name, + "hierarchy": hierarchy + } + return data + + def prepare_last_workfile(self, workdir): + """last workfile workflow preparation. + + Function check if should care about last workfile workflow and tries + to find the last workfile. Both information are stored to `data` and + environments. + + Last workfile is filled always (with version 1) even if any workfile + exists yet. + + Args: + workdir (str): Path to folder where workfiles should be stored. + """ + _workdir_data = self.launch_context.data.get("workdir_data") + if not _workdir_data: + self.log.info( + "Skipping last workfile preparation." + " Key `workdir_data` not filled." + ) + return + + workdir_data = copy.deepcopy(_workdir_data) + project_name = self.launch_context.data["project_name"] + task_name = self.launch_context.data["task_name"] + start_last_workfile = self.should_start_last_workfile( + project_name, self.launch_context.host_name, task_name + ) + self.launch_context.data["start_last_workfile"] = start_last_workfile + + # Store boolean as "0"(False) or "1"(True) + self.launch_context.env["AVALON_OPEN_LAST_WORKFILE"] = ( + str(int(bool(start_last_workfile))) + ) + + _sub_msg = "" if start_last_workfile else " not" + self.log.debug( + "Last workfile should{} be opened on start.".format(_sub_msg) + ) + + # Last workfile path + last_workfile_path = "" + extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get( + self.launch_context.host_name + ) + if extensions: + anatomy = self.launch_context.data["anatomy"] + # Find last workfile + file_template = anatomy.templates["work"]["file"] + workdir_data.update({ + "version": 1, + "user": os.environ.get("PYPE_USERNAME") or getpass.getuser(), + "ext": extensions[0] + }) + + last_workfile_path = avalon.api.last_workfile( + workdir, file_template, workdir_data, extensions, True + ) + + if os.path.exists(last_workfile_path): + self.log.debug(( + "Workfiles for launch context does not exists" + " yet but path will be set." + )) + self.log.debug( + "Setting last workfile path: {}".format(last_workfile_path) + ) + + self.launch_context.env["AVALON_LAST_WORKFILE"] = last_workfile_path + self.launch_context.data["last_workfile_path"] = last_workfile_path + + def should_start_last_workfile(self, project_name, host_name, task_name): + """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. + + """ + default_output = env_value_to_bool( + "AVALON_OPEN_LAST_WORKFILE", default=False + ) + # TODO convert to settings + try: + startup_presets = ( + config.get_presets(project_name) + .get("tools", {}) + .get("workfiles", {}) + .get("last_workfile_on_startup") + ) + except Exception: + startup_presets = None + self.log.warning("Couldn't load pype's presets", exc_info=True) + + if not startup_presets: + return default_output + + host_name_lowered = host_name.lower() + task_name_lowered = task_name.lower() + + max_points = 2 + matching_points = -1 + matching_item = None + for item in startup_presets: + hosts = item.get("hosts") or tuple() + tasks = item.get("tasks") or tuple() + + hosts_lowered = tuple(_host_name.lower() for _host_name in hosts) + # Skip item if has set hosts and current host is not in + if hosts_lowered and host_name_lowered not in hosts_lowered: + continue + + tasks_lowered = tuple(_task_name.lower() for _task_name in tasks) + # Skip item if has set tasks and current task is not in + if tasks_lowered: + task_match = False + for task_regex in self.compile_list_of_regexes(tasks_lowered): + if re.match(task_regex, task_name_lowered): + task_match = True + break + + if not task_match: + continue + + points = int(bool(hosts_lowered)) + int(bool(tasks_lowered)) + if points > matching_points: + matching_item = item + matching_points = points + + if matching_points == max_points: + break + + if matching_item is not None: + output = matching_item.get("enabled") + if output is None: + output = default_output + return output + return default_output + + @staticmethod + def compile_list_of_regexes(in_list): + """Convert strings in entered list to compiled regex objects.""" + regexes = list() + if not in_list: + return regexes + + for item in in_list: + if item: + try: + regexes.append(re.compile(item)) + except TypeError: + print(( + "Invalid type \"{}\" value \"{}\"." + " Expected string based object. Skipping." + ).format(str(type(item)), str(item))) + return regexes diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 675b0c90be..3804db1ed1 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -29,7 +29,7 @@ from .python_module_tools import ( ) from .hooks import execute_hook from .deprecated import get_avalon_database -from .env_tools import env_value_to_bool + log = logging.getLogger(__name__) @@ -80,24 +80,6 @@ class ApplicationLaunchFailed(Exception): pass -def compile_list_of_regexes(in_list): - """Convert strings in entered list to compiled regex objects.""" - regexes = list() - if not in_list: - return regexes - - for item in in_list: - if item: - try: - regexes.append(re.compile(item)) - except TypeError: - print(( - "Invalid type \"{}\" value \"{}\"." - " Expected string based object. Skipping." - ).format(str(type(item)), str(item))) - return regexes - - def launch_application(project_name, asset_name, task_name, app_name): """Launch host application with filling required environments. @@ -878,10 +860,6 @@ class ApplicationLaunchContext: self.process = None - # TODO move these to pre-paunch hook - self.prepare_global_data() - self.prepare_host_environments() - self.prepare_context_environments() @property def env(self): if ( @@ -933,8 +911,14 @@ class ApplicationLaunchContext: self.prelaunch_hooks.clear() self.postlaunch_hooks.clear() - classes = [] + self.log.debug("Discovery of launch hooks started.") + paths = self.paths_to_launch_hooks() + self.log.debug("Paths where will look for launch hooks:{}".format( + "\n- ".join(paths) + )) + + classes = [] for path in paths: if not os.path.exists(path): self.log.info( @@ -997,6 +981,9 @@ class ApplicationLaunchContext: self.prelaunch_hooks = pre_hooks_ordered self.postlaunch_hooks = post_hooks_ordered + self.log.debug("Found {} prelaunch and {} postlaunch hooks.".format( + len(self.prelaunch_hooks), len(self.postlaunch_hooks) + )) @property def app_name(self): @@ -1027,6 +1014,9 @@ class ApplicationLaunchContext: # Execute prelaunch hooks for prelaunch_hook in self.prelaunch_hooks: + self.log.debug("Executing prelaunch hook: {}".format( + str(prelaunch_hook) + )) prelaunch_hook.execute() # Prepare subprocess args @@ -1039,6 +1029,10 @@ class ApplicationLaunchContext: # Process post launch hooks for postlaunch_hook in self.postlaunch_hooks: + self.log.debug("Executing postlaunch hook: {}".format( + str(prelaunch_hook) + )) + # TODO how to handle errors? # - store to variable to let them accesible? try: @@ -1087,311 +1081,6 @@ class ApplicationLaunchContext: break return args - def prepare_global_data(self): - """Prepare global objects to `data` that will be used for sure.""" - # Mongo documents - project_name = self.data.get("project_name") - if not project_name: - self.log.info( - "Skipping global data preparation." - " Key `project_name` was not found in launch context." - ) - return - - self.log.debug("Project name is set to \"{}\"".format(project_name)) - # Anatomy - self.data["anatomy"] = Anatomy(project_name) - - # Mongo connection - dbcon = avalon.api.AvalonMongoDB() - dbcon.Session["AVALON_PROJECT"] = project_name - dbcon.install() - - self.data["dbcon"] = dbcon - - # Project document - project_doc = dbcon.find_one({"type": "project"}) - self.data["project_doc"] = project_doc - - asset_name = self.data.get("asset_name") - if not asset_name: - self.log.warning( - "Asset name was not set. Skipping asset document query." - ) - return - - asset_doc = dbcon.find_one({ - "type": "asset", - "name": asset_name - }) - self.data["asset_doc"] = asset_doc - - def _merge_env(self, env, current_env): - """Modified function(merge) from acre module.""" - result = current_env.copy() - for key, value in env.items(): - # Keep missing keys by not filling `missing` kwarg - value = acre.lib.partial_format(value, data=current_env) - result[key] = value - return result - - def prepare_host_environments(self): - """Modify launch environments based on launched app and context.""" - # Keys for getting environments - env_keys = [self.host_name, self.app_name] - - asset_doc = self.data.get("asset_doc") - if asset_doc: - # Add tools environments - for key in asset_doc["data"].get("tools_env") or []: - tool = self.manager.tools.get(key) - if tool: - if tool.group_name not in env_keys: - env_keys.append(tool.group_name) - - if tool.name not in env_keys: - env_keys.append(tool.name) - - self.log.debug( - "Finding environment groups for keys: {}".format(env_keys) - ) - - settings_env = self.data["settings_env"] - env_values = {} - for env_key in env_keys: - _env_values = settings_env.get(env_key) - if not _env_values: - continue - - # Choose right platform - tool_env = acre.parse(_env_values) - # Merge dictionaries - env_values = self._merge_env(tool_env, env_values) - - final_env = self._merge_env(acre.compute(env_values), self.env) - - # Update env - self.env.update(final_env) - - def prepare_context_environments(self): - """Modify launch environemnts with context data for launched host.""" - # Context environments - project_doc = self.data.get("project_doc") - asset_doc = self.data.get("asset_doc") - task_name = self.data.get("task_name") - if ( - not project_doc - or not asset_doc - or not task_name - ): - self.log.info( - "Skipping context environments preparation." - " Launch context does not contain required data." - ) - return - - workdir_data = self._prepare_workdir_data( - project_doc, asset_doc, task_name - ) - self.data["workdir_data"] = workdir_data - - hierarchy = workdir_data["hierarchy"] - anatomy = self.data["anatomy"] - - try: - anatomy_filled = anatomy.format(workdir_data) - workdir = os.path.normpath(anatomy_filled["work"]["folder"]) - if not os.path.exists(workdir): - self.log.debug( - "Creating workdir folder: \"{}\"".format(workdir) - ) - os.makedirs(workdir) - - except Exception as exc: - raise ApplicationLaunchFailed( - "Error in anatomy.format: {}".format(str(exc)) - ) - - context_env = { - "AVALON_PROJECT": project_doc["name"], - "AVALON_ASSET": asset_doc["name"], - "AVALON_TASK": task_name, - "AVALON_APP": self.host_name, - "AVALON_APP_NAME": self.app_name, - "AVALON_HIERARCHY": hierarchy, - "AVALON_WORKDIR": workdir - } - self.log.debug( - "Context environemnts set:\n{}".format( - json.dumps(context_env, indent=4) - ) - ) - self.env.update(context_env) - - self.prepare_last_workfile(workdir) - - def _prepare_workdir_data(self, project_doc, asset_doc, task_name): - hierarchy = "/".join(asset_doc["data"]["parents"]) - - data = { - "project": { - "name": project_doc["name"], - "code": project_doc["data"].get("code") - }, - "task": task_name, - "asset": asset_doc["name"], - "app": self.host_name, - "hierarchy": hierarchy - } - return data - - def prepare_last_workfile(self, workdir): - """last workfile workflow preparation. - - Function check if should care about last workfile workflow and tries - to find the last workfile. Both information are stored to `data` and - environments. - - Last workfile is filled always (with version 1) even if any workfile - exists yet. - - Args: - workdir (str): Path to folder where workfiles should be stored. - """ - _workdir_data = self.data.get("workdir_data") - if not _workdir_data: - self.log.info( - "Skipping last workfile preparation." - " Key `workdir_data` not filled." - ) - return - - workdir_data = copy.deepcopy(_workdir_data) - project_name = self.data["project_name"] - task_name = self.data["task_name"] - start_last_workfile = self.should_start_last_workfile( - project_name, self.host_name, task_name - ) - self.data["start_last_workfile"] = start_last_workfile - - # Store boolean as "0"(False) or "1"(True) - self.env["AVALON_OPEN_LAST_WORKFILE"] = ( - str(int(bool(start_last_workfile))) - ) - - _sub_msg = "" if start_last_workfile else " not" - self.log.debug( - "Last workfile should{} be opened on start.".format(_sub_msg) - ) - - # Last workfile path - last_workfile_path = "" - extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get(self.host_name) - if extensions: - anatomy = self.data["anatomy"] - # Find last workfile - file_template = anatomy.templates["work"]["file"] - workdir_data.update({ - "version": 1, - "user": os.environ.get("PYPE_USERNAME") or getpass.getuser(), - "ext": extensions[0] - }) - - last_workfile_path = avalon.api.last_workfile( - workdir, file_template, workdir_data, extensions, True - ) - - if os.path.exists(last_workfile_path): - self.log.debug(( - "Workfiles for launch context does not exists" - " yet but path will be set." - )) - self.log.debug( - "Setting last workfile path: {}".format(last_workfile_path) - ) - - self.env["AVALON_LAST_WORKFILE"] = last_workfile_path - self.data["last_workfile_path"] = last_workfile_path - - def should_start_last_workfile(self, project_name, host_name, task_name): - """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. - - """ - default_output = env_value_to_bool( - "AVALON_OPEN_LAST_WORKFILE", default=False - ) - # TODO convert to settings - try: - startup_presets = ( - config.get_presets(project_name) - .get("tools", {}) - .get("workfiles", {}) - .get("last_workfile_on_startup") - ) - except Exception: - startup_presets = None - self.log.warning("Couldn't load pype's presets", exc_info=True) - - if not startup_presets: - return default_output - - host_name_lowered = host_name.lower() - task_name_lowered = task_name.lower() - - max_points = 2 - matching_points = -1 - matching_item = None - for item in startup_presets: - hosts = item.get("hosts") or tuple() - tasks = item.get("tasks") or tuple() - - hosts_lowered = tuple(_host_name.lower() for _host_name in hosts) - # Skip item if has set hosts and current host is not in - if hosts_lowered and host_name_lowered not in hosts_lowered: - continue - - tasks_lowered = tuple(_task_name.lower() for _task_name in tasks) - # Skip item if has set tasks and current task is not in - if tasks_lowered: - task_match = False - for task_regex in compile_list_of_regexes(tasks_lowered): - if re.match(task_regex, task_name_lowered): - task_match = True - break - - if not task_match: - continue - - points = int(bool(hosts_lowered)) + int(bool(tasks_lowered)) - if points > matching_points: - matching_item = item - matching_points = points - - if matching_points == max_points: - break - - if matching_item is not None: - output = matching_item.get("enabled") - if output is None: - output = default_output - return output - return default_output - def after_launch_procedures(self): self._ftrack_after_launch_procedure() From a1a48fbb7ae1ad04582519bb899200e99d44d480 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 17:30:30 +0100 Subject: [PATCH 13/54] moved ftrack logic from application context to post launch hook --- pype/hooks/global/post_ftrack_changes.py | 184 +++++++++++++++++++++++ pype/lib/applications.py | 167 -------------------- 2 files changed, 184 insertions(+), 167 deletions(-) create mode 100644 pype/hooks/global/post_ftrack_changes.py diff --git a/pype/hooks/global/post_ftrack_changes.py b/pype/hooks/global/post_ftrack_changes.py new file mode 100644 index 0000000000..6dfde5409a --- /dev/null +++ b/pype/hooks/global/post_ftrack_changes.py @@ -0,0 +1,184 @@ +import os + +import ftrack_api +from pype.api import config +from pype.lib import LaunchHook + + +class PostFtrackHook(LaunchHook): + order = None + + def execute(self): + project_name = self.data.get("project_name") + asset_name = self.data.get("asset_name") + task_name = self.data.get("task_name") + + missing_context_keys = set() + if not project_name: + missing_context_keys.add("project_name") + if not asset_name: + missing_context_keys.add("asset_name") + if not task_name: + missing_context_keys.add("task_name") + + if missing_context_keys: + missing_keys_str = ", ".join([ + "\"{}\"".format(key) for key in missing_context_keys + ]) + self.log.debug("Hook {} skipped. Missing data keys: {}".format( + self.__class__.__name__, missing_keys_str + )) + return + + required_keys = ("FTRACK_SERVER", "FTRACK_API_USER", "FTRACK_API_KEY") + for key in required_keys: + if not os.environ.get(key): + self.log.debug(( + "Missing required environment \"{}\"" + " for Ftrack after launch procedure." + ).format(key)) + return + + try: + session = ftrack_api.Session(auto_connect_event_hub=True) + self.log.debug("Ftrack session created") + except Exception: + self.log.warning("Couldn't create Ftrack session") + return + + try: + entity = self.find_ftrack_task_entity( + session, project_name, asset_name, task_name + ) + if entity: + self.ftrack_status_change(session, entity, project_name) + self.start_timer(session, entity, ftrack_api) + except Exception: + self.log.warning( + "Couldn't finish Ftrack procedure.", exc_info=True + ) + return + + finally: + session.close() + + def find_ftrack_task_entity( + self, session, project_name, asset_name, task_name + ): + project_entity = session.query( + "Project where full_name is \"{}\"".format(project_name) + ).first() + if not project_entity: + self.log.warning( + "Couldn't find project \"{}\" in Ftrack.".format(project_name) + ) + return + + potential_task_entities = session.query(( + "TypedContext where parent.name is \"{}\" and project_id is \"{}\"" + ).format(asset_name, project_entity["id"])).all() + filtered_entities = [] + for _entity in potential_task_entities: + if ( + _entity.entity_type.lower() == "task" + and _entity["name"] == task_name + ): + filtered_entities.append(_entity) + + if not filtered_entities: + self.log.warning(( + "Couldn't find task \"{}\" under parent \"{}\" in Ftrack." + ).format(task_name, asset_name)) + return + + if len(filtered_entities) > 1: + self.log.warning(( + "Found more than one task \"{}\"" + " under parent \"{}\" in Ftrack." + ).format(task_name, asset_name)) + return + + return filtered_entities[0] + + def ftrack_status_change(self, session, entity, project_name): + # TODO use settings + presets = config.get_presets(project_name)["ftrack"]["ftrack_config"] + statuses = presets.get("status_update") + if not statuses: + return + + actual_status = entity["status"]["name"].lower() + already_tested = set() + ent_path = "/".join( + [ent["name"] for ent in entity["link"]] + ) + while True: + next_status_name = None + for key, value in statuses.items(): + if key in already_tested: + continue + if actual_status in value or "_any_" in value: + if key != "_ignore_": + next_status_name = key + already_tested.add(key) + break + already_tested.add(key) + + if next_status_name is None: + break + + try: + query = "Status where name is \"{}\"".format( + next_status_name + ) + status = session.query(query).one() + + entity["status"] = status + session.commit() + self.log.debug("Changing status to \"{}\" <{}>".format( + next_status_name, ent_path + )) + break + + except Exception: + session.rollback() + msg = ( + "Status \"{}\" in presets wasn't found" + " on Ftrack entity type \"{}\"" + ).format(next_status_name, entity.entity_type) + self.log.warning(msg) + + def start_timer(self, session, entity, _ftrack_api): + """Start Ftrack timer on task from context.""" + self.log.debug("Triggering timer start.") + + user_entity = session.query("User where username is \"{}\"".format( + os.environ["FTRACK_API_USER"] + )).first() + if not user_entity: + self.log.warning( + "Couldn't find user with username \"{}\" in Ftrack".format( + os.environ["FTRACK_API_USER"] + ) + ) + return + + source = { + "user": { + "id": user_entity["id"], + "username": user_entity["username"] + } + } + event_data = { + "actionIdentifier": "start.timer", + "selection": [{"entityId": entity["id"], "entityType": "task"}] + } + session.event_hub.publish( + _ftrack_api.event.base.Event( + topic="ftrack.action.launch", + data=event_data, + source=source + ), + on_error="ignore" + ) + self.log.debug("Timer start triggered successfully.") diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 3804db1ed1..47cb1c7176 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -1080,170 +1080,3 @@ class ApplicationLaunchContext: if all_cleared: break return args - - def after_launch_procedures(self): - self._ftrack_after_launch_procedure() - - def _ftrack_after_launch_procedure(self): - # TODO move to launch hook - project_name = self.data.get("project_name") - asset_name = self.data.get("asset_name") - task_name = self.data.get("task_name") - if ( - not project_name - or not asset_name - or not task_name - ): - return - - required_keys = ("FTRACK_SERVER", "FTRACK_API_USER", "FTRACK_API_KEY") - for key in required_keys: - if not os.environ.get(key): - self.log.debug(( - "Missing required environment \"{}\"" - " for Ftrack after launch procedure." - ).format(key)) - return - - try: - import ftrack_api - session = ftrack_api.Session(auto_connect_event_hub=True) - self.log.debug("Ftrack session created") - except Exception: - self.log.warning("Couldn't create Ftrack session") - return - - try: - entity = self._find_ftrack_task_entity( - session, project_name, asset_name, task_name - ) - self._ftrack_status_change(session, entity, project_name) - self._start_timer(session, entity, ftrack_api) - except Exception: - self.log.warning( - "Couldn't finish Ftrack procedure.", exc_info=True - ) - return - - finally: - session.close() - - def _find_ftrack_task_entity( - self, session, project_name, asset_name, task_name - ): - project_entity = session.query( - "Project where full_name is \"{}\"".format(project_name) - ).first() - if not project_entity: - self.log.warning( - "Couldn't find project \"{}\" in Ftrack.".format(project_name) - ) - return - - potential_task_entities = session.query(( - "TypedContext where parent.name is \"{}\" and project_id is \"{}\"" - ).format(asset_name, project_entity["id"])).all() - filtered_entities = [] - for _entity in potential_task_entities: - if ( - _entity.entity_type.lower() == "task" - and _entity["name"] == task_name - ): - filtered_entities.append(_entity) - - if not filtered_entities: - self.log.warning(( - "Couldn't find task \"{}\" under parent \"{}\" in Ftrack." - ).format(task_name, asset_name)) - return - - if len(filtered_entities) > 1: - self.log.warning(( - "Found more than one task \"{}\"" - " under parent \"{}\" in Ftrack." - ).format(task_name, asset_name)) - return - - return filtered_entities[0] - - def _ftrack_status_change(self, session, entity, project_name): - from pype.api import config - presets = config.get_presets(project_name)["ftrack"]["ftrack_config"] - statuses = presets.get("status_update") - if not statuses: - return - - actual_status = entity["status"]["name"].lower() - already_tested = set() - ent_path = "/".join( - [ent["name"] for ent in entity["link"]] - ) - while True: - next_status_name = None - for key, value in statuses.items(): - if key in already_tested: - continue - if actual_status in value or "_any_" in value: - if key != "_ignore_": - next_status_name = key - already_tested.add(key) - break - already_tested.add(key) - - if next_status_name is None: - break - - try: - query = "Status where name is \"{}\"".format( - next_status_name - ) - status = session.query(query).one() - - entity["status"] = status - session.commit() - self.log.debug("Changing status to \"{}\" <{}>".format( - next_status_name, ent_path - )) - break - - except Exception: - session.rollback() - msg = ( - "Status \"{}\" in presets wasn't found" - " on Ftrack entity type \"{}\"" - ).format(next_status_name, entity.entity_type) - self.log.warning(msg) - - def _start_timer(self, session, entity, _ftrack_api): - self.log.debug("Triggering timer start.") - - user_entity = session.query("User where username is \"{}\"".format( - os.environ["FTRACK_API_USER"] - )).first() - if not user_entity: - self.log.warning( - "Couldn't find user with username \"{}\" in Ftrack".format( - os.environ["FTRACK_API_USER"] - ) - ) - return - - source = { - "user": { - "id": user_entity["id"], - "username": user_entity["username"] - } - } - event_data = { - "actionIdentifier": "start.timer", - "selection": [{"entityId": entity["id"], "entityType": "task"}] - } - session.event_hub.publish( - _ftrack_api.event.base.Event( - topic="ftrack.action.launch", - data=event_data, - source=source - ), - on_error="ignore" - ) - self.log.debug("Timer start triggered successfully.") From f97ae3f2690845426cb1856d248fb82e55084a5c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 17:30:48 +0100 Subject: [PATCH 14/54] renamed file to start with pre_* --- ...l_host_data.py => pre_global_host_data.py} | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) rename pype/hooks/global/{global_host_data.py => pre_global_host_data.py} (87%) diff --git a/pype/hooks/global/global_host_data.py b/pype/hooks/global/pre_global_host_data.py similarity index 87% rename from pype/hooks/global/global_host_data.py rename to pype/hooks/global/pre_global_host_data.py index 3f82f3b099..0b501212a0 100644 --- a/pype/hooks/global/global_host_data.py +++ b/pype/hooks/global/pre_global_host_data.py @@ -38,7 +38,7 @@ class GlobalHostDataHook(LaunchHook): def prepare_global_data(self): """Prepare global objects to `data` that will be used for sure.""" # Mongo documents - project_name = self.launch_context.data.get("project_name") + project_name = self.data.get("project_name") if not project_name: self.log.info( "Skipping global data preparation." @@ -48,20 +48,20 @@ class GlobalHostDataHook(LaunchHook): self.log.debug("Project name is set to \"{}\"".format(project_name)) # Anatomy - self.launch_context.data["anatomy"] = Anatomy(project_name) + self.data["anatomy"] = Anatomy(project_name) # Mongo connection dbcon = avalon.api.AvalonMongoDB() dbcon.Session["AVALON_PROJECT"] = project_name dbcon.install() - self.launch_context.data["dbcon"] = dbcon + self.data["dbcon"] = dbcon # Project document project_doc = dbcon.find_one({"type": "project"}) - self.launch_context.data["project_doc"] = project_doc + self.data["project_doc"] = project_doc - asset_name = self.launch_context.data.get("asset_name") + asset_name = self.data.get("asset_name") if not asset_name: self.log.warning( "Asset name was not set. Skipping asset document query." @@ -72,7 +72,7 @@ class GlobalHostDataHook(LaunchHook): "type": "asset", "name": asset_name }) - self.launch_context.data["asset_doc"] = asset_doc + self.data["asset_doc"] = asset_doc def _merge_env(self, env, current_env): """Modified function(merge) from acre module.""" @@ -86,12 +86,9 @@ class GlobalHostDataHook(LaunchHook): def prepare_host_environments(self): """Modify launch environments based on launched app and context.""" # Keys for getting environments - env_keys = [ - self.launch_context.host_name, - self.launch_context.app_name - ] + env_keys = [self.host_name, self.app_name] - asset_doc = self.launch_context.data.get("asset_doc") + asset_doc = self.data.get("asset_doc") if asset_doc: # Add tools environments for key in asset_doc["data"].get("tools_env") or []: @@ -107,7 +104,7 @@ class GlobalHostDataHook(LaunchHook): "Finding environment groups for keys: {}".format(env_keys) ) - settings_env = self.launch_context.data["settings_env"] + settings_env = self.data["settings_env"] env_values = {} for env_key in env_keys: _env_values = settings_env.get(env_key) @@ -129,9 +126,9 @@ class GlobalHostDataHook(LaunchHook): def prepare_context_environments(self): """Modify launch environemnts with context data for launched host.""" # Context environments - project_doc = self.launch_context.data.get("project_doc") - asset_doc = self.launch_context.data.get("asset_doc") - task_name = self.launch_context.data.get("task_name") + project_doc = self.data.get("project_doc") + asset_doc = self.data.get("asset_doc") + task_name = self.data.get("task_name") if ( not project_doc or not asset_doc @@ -146,10 +143,10 @@ class GlobalHostDataHook(LaunchHook): workdir_data = self._prepare_workdir_data( project_doc, asset_doc, task_name ) - self.launch_context.data["workdir_data"] = workdir_data + self.data["workdir_data"] = workdir_data hierarchy = workdir_data["hierarchy"] - anatomy = self.launch_context.data["anatomy"] + anatomy = self.data["anatomy"] try: anatomy_filled = anatomy.format(workdir_data) @@ -169,8 +166,8 @@ class GlobalHostDataHook(LaunchHook): "AVALON_PROJECT": project_doc["name"], "AVALON_ASSET": asset_doc["name"], "AVALON_TASK": task_name, - "AVALON_APP": self.launch_context.host_name, - "AVALON_APP_NAME": self.launch_context.app_name, + "AVALON_APP": self.host_name, + "AVALON_APP_NAME": self.app_name, "AVALON_HIERARCHY": hierarchy, "AVALON_WORKDIR": workdir } @@ -193,7 +190,7 @@ class GlobalHostDataHook(LaunchHook): }, "task": task_name, "asset": asset_doc["name"], - "app": self.launch_context.host_name, + "app": self.host_name, "hierarchy": hierarchy } return data @@ -211,7 +208,7 @@ class GlobalHostDataHook(LaunchHook): Args: workdir (str): Path to folder where workfiles should be stored. """ - _workdir_data = self.launch_context.data.get("workdir_data") + _workdir_data = self.data.get("workdir_data") if not _workdir_data: self.log.info( "Skipping last workfile preparation." @@ -220,12 +217,12 @@ class GlobalHostDataHook(LaunchHook): return workdir_data = copy.deepcopy(_workdir_data) - project_name = self.launch_context.data["project_name"] - task_name = self.launch_context.data["task_name"] + project_name = self.data["project_name"] + task_name = self.data["task_name"] start_last_workfile = self.should_start_last_workfile( - project_name, self.launch_context.host_name, task_name + project_name, self.host_name, task_name ) - self.launch_context.data["start_last_workfile"] = start_last_workfile + self.data["start_last_workfile"] = start_last_workfile # Store boolean as "0"(False) or "1"(True) self.launch_context.env["AVALON_OPEN_LAST_WORKFILE"] = ( @@ -240,10 +237,10 @@ class GlobalHostDataHook(LaunchHook): # Last workfile path last_workfile_path = "" extensions = avalon.api.HOST_WORKFILE_EXTENSIONS.get( - self.launch_context.host_name + self.host_name ) if extensions: - anatomy = self.launch_context.data["anatomy"] + anatomy = self.data["anatomy"] # Find last workfile file_template = anatomy.templates["work"]["file"] workdir_data.update({ @@ -266,7 +263,7 @@ class GlobalHostDataHook(LaunchHook): ) self.launch_context.env["AVALON_LAST_WORKFILE"] = last_workfile_path - self.launch_context.data["last_workfile_path"] = last_workfile_path + self.data["last_workfile_path"] = last_workfile_path def should_start_last_workfile(self, project_name, host_name, task_name): """Define if host should start last version workfile if possible. From 88316fe4211546d5290faf7204f58506e1ff4264 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 17:56:58 +0100 Subject: [PATCH 15/54] prelaunch and postlaunch hooks are diferentiated by inherit class --- pype/hooks/global/post_ftrack_changes.py | 4 +- pype/hooks/global/pre_global_host_data.py | 4 +- pype/lib/__init__.py | 6 +- pype/lib/applications.py | 127 +++++++++++++--------- 4 files changed, 82 insertions(+), 59 deletions(-) diff --git a/pype/hooks/global/post_ftrack_changes.py b/pype/hooks/global/post_ftrack_changes.py index 6dfde5409a..144f618620 100644 --- a/pype/hooks/global/post_ftrack_changes.py +++ b/pype/hooks/global/post_ftrack_changes.py @@ -2,10 +2,10 @@ import os import ftrack_api from pype.api import config -from pype.lib import LaunchHook +from pype.lib import PostLaunchHook -class PostFtrackHook(LaunchHook): +class PostFtrackHook(PostLaunchHook): order = None def execute(self): diff --git a/pype/hooks/global/pre_global_host_data.py b/pype/hooks/global/pre_global_host_data.py index 0b501212a0..787460019d 100644 --- a/pype/hooks/global/pre_global_host_data.py +++ b/pype/hooks/global/pre_global_host_data.py @@ -10,7 +10,7 @@ from pype.api import ( ) from pype.lib import ( env_value_to_bool, - LaunchHook, + PreLaunchHook, ApplicationLaunchFailed ) @@ -18,7 +18,7 @@ import acre import avalon.api -class GlobalHostDataHook(LaunchHook): +class GlobalHostDataHook(PreLaunchHook): order = -100 def execute(self): diff --git a/pype/lib/__init__.py b/pype/lib/__init__.py index 512cb12e37..ecdd155c99 100644 --- a/pype/lib/__init__.py +++ b/pype/lib/__init__.py @@ -34,7 +34,8 @@ from .applications import ( ApplictionExecutableNotFound, ApplicationNotFound, ApplicationManager, - LaunchHook, + PreLaunchHook, + PostLaunchHook, launch_application, ApplicationAction, _subprocess @@ -79,7 +80,8 @@ __all__ = [ "ApplictionExecutableNotFound", "ApplicationNotFound", "ApplicationManager", - "LaunchHook", + "PreLaunchHook", + "PostLaunchHook", "launch_application", "ApplicationAction", diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 47cb1c7176..9d34a8b22c 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -704,11 +704,9 @@ class Application: @six.add_metaclass(ABCMeta) class LaunchHook: - """Abstract class from all hooks should inherit.""" + """Abstract base class of launch hook.""" # Order of prelaunch hook, will be executed as last if set to None. order = None - # If hook should be executed befor or after application launch - prelaunch = True # List of host implementations, skipped if empty. hosts = [] # List of platform availability, skipped if empty. @@ -790,6 +788,26 @@ class LaunchHook: pass +class PreLaunchHook(LaunchHook): + """Abstract class of prelaunch hook. + + This launch hook will be processed before application is launched. + + If any exception will happen during processing the application won't be + launched. + """ + + +class PostLaunchHook(LaunchHook): + """Abstract class of postlaunch hook. + + This launch hook will be processed after application is launched. + + Nothing will happen if any exception will happen during processing. And + processing of other postlaunch hooks won't stop either. + """ + + class ApplicationLaunchContext: """Context of launching application. @@ -918,7 +936,10 @@ class ApplicationLaunchContext: "\n- ".join(paths) )) - classes = [] + all_classes = { + "pre": [], + "post": [] + } for path in paths: if not os.path.exists(path): self.log.info( @@ -928,59 +949,55 @@ class ApplicationLaunchContext: modules = modules_from_path(path) for _module in modules: - classes.extend(classes_from_module(LaunchHook, _module)) - - pre_hooks_with_order = [] - pre_hooks_without_order = [] - post_hooks_with_order = [] - post_hooks_without_order = [] - for klass in classes: - try: - hook = klass(self) - if not hook.is_valid: - self.log.debug( - "Hook is not valid for curent launch context." - ) - continue - - if inspect.isabstract(hook): - self.log.debug("Skipped abstract hook: {}".format( - str(hook) - )) - continue - - # Separate hooks if should be executed before or after launch - if hook.prelaunch: - if hook.order is None: - pre_hooks_without_order.append(hook) - else: - pre_hooks_with_order.append(hook) - else: - if hook.order is None: - post_hooks_with_order.append(hook) - else: - post_hooks_without_order.append(hook) - - except Exception: - self.log.warning( - "Initialization of hook failed. {}".format(str(klass)), - exc_info=True + all_classes["pre"].extend( + classes_from_module(PreLaunchHook, _module) + ) + all_classes["post"].extend( + classes_from_module(PostLaunchHook, _module) ) - # Sort hooks with order by order - pre_hooks_ordered = list(sorted( - pre_hooks_with_order, key=lambda obj: obj.order - )) - post_hooks_ordered = list(sorted( - post_hooks_with_order, key=lambda obj: obj.order - )) + for launch_type, classes in all_classes.items(): + hooks_with_order = [] + hooks_without_order = [] + for klass in classes: + try: + hook = klass(self) + if not hook.is_valid: + self.log.debug( + "Hook is not valid for curent launch context." + ) + continue - # Extend ordered hooks with hooks without defined order - pre_hooks_ordered.extend(pre_hooks_without_order) - post_hooks_ordered.extend(post_hooks_without_order) + if inspect.isabstract(hook): + self.log.debug("Skipped abstract hook: {}".format( + str(hook) + )) + continue + + # Separate hooks by pre/post class + if hook.order is None: + hooks_without_order.append(hook) + else: + hooks_with_order.append(hook) + + except Exception: + self.log.warning( + "Initialization of hook failed. {}".format(str(klass)), + exc_info=True + ) + + # Sort hooks with order by order + ordered_hooks = list(sorted( + hooks_with_order, key=lambda obj: obj.order + )) + # Extend ordered hooks with hooks without defined order + ordered_hooks.extend(hooks_without_order) + + if launch_type == "pre": + self.prelaunch_hooks = ordered_hooks + else: + self.postlaunch_hooks = ordered_hooks - self.prelaunch_hooks = pre_hooks_ordered - self.postlaunch_hooks = post_hooks_ordered self.log.debug("Found {} prelaunch and {} postlaunch hooks.".format( len(self.prelaunch_hooks), len(self.postlaunch_hooks) )) @@ -1019,6 +1036,8 @@ class ApplicationLaunchContext: )) prelaunch_hook.execute() + self.log.debug("All prelaunch hook executed. Starting new process.") + # Prepare subprocess args args = self.clear_launch_args(self.launch_args) self.log.debug( @@ -1044,6 +1063,8 @@ class ApplicationLaunchContext: exc_info=True ) + self.log.debug("Launch of {} finished.".format(self.app_name)) + return self.process @staticmethod From b1c587ae8347d97ba96e202dad219edc1cacc388 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 18:18:39 +0100 Subject: [PATCH 16/54] added few basic prelaunch hooks for nuke --- pype/hooks/hiero/pre_launch_args.py | 18 ++++++++++++++++++ pype/hooks/nukestudio/pre_launch_args.py | 18 ++++++++++++++++++ pype/hooks/nukex/pre_launch_args.py | 18 ++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 pype/hooks/hiero/pre_launch_args.py create mode 100644 pype/hooks/nukestudio/pre_launch_args.py create mode 100644 pype/hooks/nukex/pre_launch_args.py diff --git a/pype/hooks/hiero/pre_launch_args.py b/pype/hooks/hiero/pre_launch_args.py new file mode 100644 index 0000000000..20bac0ca8e --- /dev/null +++ b/pype/hooks/hiero/pre_launch_args.py @@ -0,0 +1,18 @@ +from pype.lib import PreLaunchHook + + +class HieroLaunchArguments(PreLaunchHook): + order = 0 + + def execute(self): + """Prepare suprocess launch arguments for NukeX.""" + # Get executable + executable = self.launch_context.launch_args[0] + + if isinstance(executable, str): + executable = [executable] + + # Add `nukex` argument and make sure it's bind to execuable + executable.append("--hiero") + + self.launch_context.launch_args[0] = executable diff --git a/pype/hooks/nukestudio/pre_launch_args.py b/pype/hooks/nukestudio/pre_launch_args.py new file mode 100644 index 0000000000..a1c8dda314 --- /dev/null +++ b/pype/hooks/nukestudio/pre_launch_args.py @@ -0,0 +1,18 @@ +from pype.lib import PreLaunchHook + + +class NukeStudioLaunchArguments(PreLaunchHook): + order = 0 + + def execute(self): + """Prepare suprocess launch arguments for NukeX.""" + # Get executable + executable = self.launch_context.launch_args[0] + + if isinstance(executable, str): + executable = [executable] + + # Add `nukex` argument and make sure it's bind to execuable + executable.append("--studio") + + self.launch_context.launch_args[0] = executable diff --git a/pype/hooks/nukex/pre_launch_args.py b/pype/hooks/nukex/pre_launch_args.py new file mode 100644 index 0000000000..b204cf0cbb --- /dev/null +++ b/pype/hooks/nukex/pre_launch_args.py @@ -0,0 +1,18 @@ +from pype.lib import PreLaunchHook + + +class NukeXLaunchArguments(PreLaunchHook): + order = 0 + + def execute(self): + """Prepare suprocess launch arguments for NukeX.""" + # Get executable + executable = self.launch_context.launch_args[0] + + if isinstance(executable, str): + executable = [executable] + + # Add `nukex` argument and make sure it's bind to execuable + executable.append("--nukex") + + self.launch_context.launch_args[0] = executable From 8abd04959ad7b7e3bc9f8140c2073ae84c9c6f61 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 18:25:26 +0100 Subject: [PATCH 17/54] added prelaunch hook for tvpaint for installation of pywin32 --- pype/hooks/tvpaint/pre_install_pywin.py | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 pype/hooks/tvpaint/pre_install_pywin.py diff --git a/pype/hooks/tvpaint/pre_install_pywin.py b/pype/hooks/tvpaint/pre_install_pywin.py new file mode 100644 index 0000000000..20d4b2aae7 --- /dev/null +++ b/pype/hooks/tvpaint/pre_install_pywin.py @@ -0,0 +1,34 @@ +from pype.lib import ( + PreLaunchHook, + ApplicationLaunchFailed, + _subprocess +) + + +class PreInstallPyWin(PreLaunchHook): + """Hook makes sure there is installed python module pywin32 on windows.""" + # WARNING This hook will probably be deprecated in Pype 3 - kept for test + order = 10 + hosts = ["tvpaint"] + platforms = ["windows"] + + def execute(self): + installed = False + try: + from win32com.shell import shell + self.log.debug("Python module `pywin32` already installed.") + installed = True + except Exception: + pass + + if installed: + return + + try: + output = _subprocess( + ["pip", "install", "pywin32==227"], logger=self.log + ) + except RuntimeError: + msg = "Installation of python module `pywin32` crashed." + self.log.warning(msg, exc_info=True) + raise ApplicationLaunchFailed(msg) From b9c569c159a824489832dfea5fff5b5fca8dac6a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 18:59:33 +0100 Subject: [PATCH 18/54] added hosts filtering --- pype/hooks/hiero/pre_launch_args.py | 3 ++- pype/hooks/nukestudio/pre_launch_args.py | 1 + pype/hooks/nukex/pre_launch_args.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pype/hooks/hiero/pre_launch_args.py b/pype/hooks/hiero/pre_launch_args.py index 20bac0ca8e..ff5e3d79e5 100644 --- a/pype/hooks/hiero/pre_launch_args.py +++ b/pype/hooks/hiero/pre_launch_args.py @@ -3,7 +3,8 @@ from pype.lib import PreLaunchHook class HieroLaunchArguments(PreLaunchHook): order = 0 - + hosts = ["hiero"] + def execute(self): """Prepare suprocess launch arguments for NukeX.""" # Get executable diff --git a/pype/hooks/nukestudio/pre_launch_args.py b/pype/hooks/nukestudio/pre_launch_args.py index a1c8dda314..b0ca41a614 100644 --- a/pype/hooks/nukestudio/pre_launch_args.py +++ b/pype/hooks/nukestudio/pre_launch_args.py @@ -3,6 +3,7 @@ from pype.lib import PreLaunchHook class NukeStudioLaunchArguments(PreLaunchHook): order = 0 + hosts = ["nukestudio"] def execute(self): """Prepare suprocess launch arguments for NukeX.""" diff --git a/pype/hooks/nukex/pre_launch_args.py b/pype/hooks/nukex/pre_launch_args.py index b204cf0cbb..16a83cfd63 100644 --- a/pype/hooks/nukex/pre_launch_args.py +++ b/pype/hooks/nukex/pre_launch_args.py @@ -3,6 +3,7 @@ from pype.lib import PreLaunchHook class NukeXLaunchArguments(PreLaunchHook): order = 0 + hosts = ["nukex"] def execute(self): """Prepare suprocess launch arguments for NukeX.""" From 163c2d7e7dc0e01cb8fb4347366fd7456936da09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 19:00:01 +0100 Subject: [PATCH 19/54] implemented tvpaint prelaunch hook which modify launch arguments --- pype/hooks/tvpaint/pre_launch_args.py | 99 +++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 pype/hooks/tvpaint/pre_launch_args.py diff --git a/pype/hooks/tvpaint/pre_launch_args.py b/pype/hooks/tvpaint/pre_launch_args.py new file mode 100644 index 0000000000..9f90398433 --- /dev/null +++ b/pype/hooks/tvpaint/pre_launch_args.py @@ -0,0 +1,99 @@ +import os +import shutil + +from pype.hosts import tvpaint +from pype.lib import ( + PreLaunchHook, + ApplicationLaunchFailed, + _subprocess +) +import avalon + + +class TvpaintPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and script path to tvpaint implementation before + tvpaint executable and add last workfile path to launch arguments. + + Existence of last workfile is checked. If workfile does not exists tries + to copy templated workfile from predefined path. + """ + hosts = ["tvpaint"] + + def execute(self): + tvpaint_executable = self.launch_context.launch_args.pop(0) + + # This should never be used! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + self.launch_context.launch_args.append( + self.main_executable() + ) + self.launch_context.launch_args.append( + "\"{}\"".format(self.launch_script_path()) + ) + self.launch_context.launch_args.append( + "\"{}\"".format(tvpaint_executable) + ) + + # Add workfile to launch arguments + workfile_path = self.workfile_path() + if workfile_path: + self.launch_context.launch_args.append( + "\"{}\"".format(workfile_path) + ) + + if remainders: + self.log.warning(( + "There are unexpected launch arguments in TVPaint launch. {}" + ).format(str(remainders))) + self.launch_context.launch_args.extend(remainders) + + def main_executable(self): + """Should lead to python executable.""" + # TODO change in Pype 3 + return os.environ["PYPE_PYTHON_EXE"] + + def launch_script_path(self): + avalon_dir = os.path.dirname(os.path.abspath(avalon.__file__)) + script_path = os.path.join( + avalon_dir, + "tvpaint", + "launch_script.py" + ) + return script_path + + def workfile_path(self): + workfile_path = self.data["last_workfile"] + + # copy workfile from template if doesnt exist any on path + if not os.path.exists(workfile_path): + # TODO add ability to set different template workfile path via + # settings + pype_dir = os.path.dirname(os.path.abspath(tvpaint.__file__)) + template_path = os.path.join(pype_dir, "template.tvpp") + + if not os.path.exists(template_path): + self.log.warning( + "Couldn't find workfile template file in {}".format( + template_path + ) + ) + return + + self.log.info( + f"Creating workfile from template: \"{template_path}\"" + ) + + # Copy template workfile to new destinantion + shutil.copy2( + os.path.normpath(template_path), + os.path.normpath(workfile_path) + ) + + self.log.info(f"Workfile to open: \"{workfile_path}\"") + + return workfile_path From 1e91b84017783ebe5304880037945070b15dd444 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 19:25:44 +0100 Subject: [PATCH 20/54] add last workfile if should start last workfile --- pype/hooks/hiero/pre_launch_args.py | 10 +++++++++- pype/hooks/nukestudio/pre_launch_args.py | 8 ++++++++ pype/hooks/nukex/pre_launch_args.py | 8 ++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pype/hooks/hiero/pre_launch_args.py b/pype/hooks/hiero/pre_launch_args.py index ff5e3d79e5..cb03f03b88 100644 --- a/pype/hooks/hiero/pre_launch_args.py +++ b/pype/hooks/hiero/pre_launch_args.py @@ -1,10 +1,11 @@ +import os from pype.lib import PreLaunchHook class HieroLaunchArguments(PreLaunchHook): order = 0 hosts = ["hiero"] - + def execute(self): """Prepare suprocess launch arguments for NukeX.""" # Get executable @@ -17,3 +18,10 @@ class HieroLaunchArguments(PreLaunchHook): executable.append("--hiero") self.launch_context.launch_args[0] = executable + + if self.data.get("start_last_workfile"): + last_workfile = self.data.get("last_workfile_path") + if os.path.exists(last_workfile): + self.launch_context.launch_args.append( + "\"{}\"".format(last_workfile) + ) diff --git a/pype/hooks/nukestudio/pre_launch_args.py b/pype/hooks/nukestudio/pre_launch_args.py index b0ca41a614..d567f36ad0 100644 --- a/pype/hooks/nukestudio/pre_launch_args.py +++ b/pype/hooks/nukestudio/pre_launch_args.py @@ -1,3 +1,4 @@ +import os from pype.lib import PreLaunchHook @@ -17,3 +18,10 @@ class NukeStudioLaunchArguments(PreLaunchHook): executable.append("--studio") self.launch_context.launch_args[0] = executable + + if self.data.get("start_last_workfile"): + last_workfile = self.data.get("last_workfile_path") + if os.path.exists(last_workfile): + self.launch_context.launch_args.append( + "\"{}\"".format(last_workfile) + ) diff --git a/pype/hooks/nukex/pre_launch_args.py b/pype/hooks/nukex/pre_launch_args.py index 16a83cfd63..eb0b963926 100644 --- a/pype/hooks/nukex/pre_launch_args.py +++ b/pype/hooks/nukex/pre_launch_args.py @@ -1,3 +1,4 @@ +import os from pype.lib import PreLaunchHook @@ -17,3 +18,10 @@ class NukeXLaunchArguments(PreLaunchHook): executable.append("--nukex") self.launch_context.launch_args[0] = executable + + if self.data.get("start_last_workfile"): + last_workfile = self.data.get("last_workfile_path") + if os.path.exists(last_workfile): + self.launch_context.launch_args.append( + "\"{}\"".format(last_workfile) + ) From cb07c41d92ef3cf68faafa596552ac18223b247c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 20:01:07 +0100 Subject: [PATCH 21/54] added few prelaunch hooks --- .../celaction/pre_celaction_registers.py | 128 ++++++++++++++++++ pype/hooks/fusion/pre_fusion_setup.py | 50 +++++++ pype/hooks/photoshop/pre_launch_args.py | 53 ++++++++ pype/hooks/tvpaint/pre_launch_args.py | 20 +-- 4 files changed, 241 insertions(+), 10 deletions(-) create mode 100644 pype/hooks/celaction/pre_celaction_registers.py create mode 100644 pype/hooks/fusion/pre_fusion_setup.py create mode 100644 pype/hooks/photoshop/pre_launch_args.py diff --git a/pype/hooks/celaction/pre_celaction_registers.py b/pype/hooks/celaction/pre_celaction_registers.py new file mode 100644 index 0000000000..c638ce3c0b --- /dev/null +++ b/pype/hooks/celaction/pre_celaction_registers.py @@ -0,0 +1,128 @@ +import os +import shutil +import winreg +from pype.lib import PreLaunchHook +from pype.hosts import celaction + + +class CelactionPrelaunchHook(PreLaunchHook): + """ + 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. + """ + workfile_ext = "scn" + hosts = ["celaction"] + platforms = ["windows"] + + def execute(self): + # Add workfile path to launch arguments + workfile_path = self.workfile_path() + if workfile_path: + self.launch_context.launch_args.append( + "\"{}\"".format(workfile_path) + ) + + project_name = self.data["project_name"] + asset_name = self.data["asset_name"] + task_name = self.data["task_name"] + + # get publish version of celaction + app = "celaction_publish" + + # setting output parameters + path = r"Software\CelAction\CelAction2D\User Settings" + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + "Software\\CelAction\\CelAction2D\\User Settings", 0, + winreg.KEY_ALL_ACCESS) + + # TODO: change to root path and pyblish standalone to premiere way + pype_root_path = os.getenv("PYPE_SETUP_PATH") + path = os.path.join(pype_root_path, + "pype.bat") + + winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, path) + + parameters = [ + "launch", + f"--app {app}", + f"--project {project_name}", + f"--asset {asset_name}", + f"--task {task_name}", + "--currentFile \\\"\"*SCENE*\"\\\"", + "--chunk 10", + "--frameStart *START*", + "--frameEnd *END*", + "--resolutionWidth *X*", + "--resolutionHeight *Y*", + # "--programDir \"'*PROGPATH*'\"" + ] + winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, + " ".join(parameters)) + + # setting resolution parameters + path = r"Software\CelAction\CelAction2D\User Settings\Dialogs" + path += r"\SubmitOutput" + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "SaveScene", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(hKey, "CustomX", 0, winreg.REG_DWORD, 1920) + winreg.SetValueEx(hKey, "CustomY", 0, winreg.REG_DWORD, 1080) + + # making sure message dialogs don't appear when overwriting + path = r"Software\CelAction\CelAction2D\User Settings\Messages" + path += r"\OverwriteScene" + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 6) + winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) + + path = r"Software\CelAction\CelAction2D\User Settings\Messages" + path += r"\SceneSaved" + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) + + def workfile_path(self): + workfile_path = self.data["last_workfile"] + + # copy workfile from template if doesnt exist any on path + if not os.path.exists(workfile_path): + # TODO add ability to set different template workfile path via + # settings + pype_celaction_dir = os.path.dirname( + os.path.abspath(celaction.__file__) + ) + template_path = os.path.join( + pype_celaction_dir, + "celaction_template_scene.scn" + ) + + if not os.path.exists(template_path): + self.log.warning( + "Couldn't find workfile template file in {}".format( + template_path + ) + ) + return + + self.log.info( + f"Creating workfile from template: \"{template_path}\"" + ) + + # Copy template workfile to new destinantion + shutil.copy2( + os.path.normpath(template_path), + os.path.normpath(workfile_path) + ) + + self.log.info(f"Workfile to open: \"{workfile_path}\"") + + return workfile_path diff --git a/pype/hooks/fusion/pre_fusion_setup.py b/pype/hooks/fusion/pre_fusion_setup.py new file mode 100644 index 0000000000..ac7dda4250 --- /dev/null +++ b/pype/hooks/fusion/pre_fusion_setup.py @@ -0,0 +1,50 @@ +import os +import importlib +from pype.lib import PreLaunchHook +from pype.hosts.fusion import utils + + +class FusionPrelaunch(PreLaunchHook): + """ + This hook will check if current workfile path has Fusion + project inside. + """ + hosts = ["fusion"] + + def execute(self): + # making sure pyton 3.6 is installed at provided path + py36_dir = os.path.normpath(self.env.get("PYTHON36", "")) + assert os.path.isdir(py36_dir), ( + "Python 3.6 is not installed at the provided folder path. Either " + "make sure the `environments\resolve.json` is having correctly " + "set `PYTHON36` or make sure Python 3.6 is installed " + f"in given path. \nPYTHON36E: `{py36_dir}`" + ) + self.log.info(f"Path to Fusion Python folder: `{py36_dir}`...") + self.env["PYTHON36"] = py36_dir + + # setting utility scripts dir for scripts syncing + us_dir = os.path.normpath( + self.env.get("FUSION_UTILITY_SCRIPTS_DIR", "") + ) + assert os.path.isdir(us_dir), ( + "Fusion utility script dir does not exists. Either make sure " + "the `environments\fusion.json` is having correctly set " + "`FUSION_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n" + f"FUSION_UTILITY_SCRIPTS_DIR: `{us_dir}`" + ) + + try: + __import__("avalon.fusion") + __import__("pyblish") + + except ImportError: + self.log.warning( + "pyblish: Could not load Fusion integration.", + exc_info=True + ) + + else: + # Resolve Setup integration + importlib.reload(utils) + utils.setup(self.env) diff --git a/pype/hooks/photoshop/pre_launch_args.py b/pype/hooks/photoshop/pre_launch_args.py new file mode 100644 index 0000000000..bc747dc495 --- /dev/null +++ b/pype/hooks/photoshop/pre_launch_args.py @@ -0,0 +1,53 @@ +import os +import platform + +from pype.lib import PreLaunchHook + + +class PhotoshopPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and execute python script of photoshop + implementation before photoshop executable. + """ + hosts = ["photoshop"] + + def execute(self): + # Pop tvpaint executable + photoshop_executable = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + python_launch_args = [ + self.python_executable(), + "-c", + ( + "^\"import avalon.photoshop;" + "avalon.photoshop.launch(\"{}\")^\"\"" + ).format(photoshop_executable) + ] + if platform.system().lower() != "windows": + new_launch_args = python_launch_args + else: + new_launch_args = [ + "cmd.exe", + "/k", + "\"{}\"".format(" ".join(python_launch_args)) + ] + + # Append as whole list as these areguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.log.warning(( + "There are unexpected launch arguments in Photoshop launch. {}" + ).format(str(remainders))) + self.launch_context.launch_args.extend(remainders) + + def python_executable(self): + """Should lead to python executable.""" + # TODO change in Pype 3 + return os.environ["PYPE_PYTHON_EXE"] diff --git a/pype/hooks/tvpaint/pre_launch_args.py b/pype/hooks/tvpaint/pre_launch_args.py index 9f90398433..6c59a8ce20 100644 --- a/pype/hooks/tvpaint/pre_launch_args.py +++ b/pype/hooks/tvpaint/pre_launch_args.py @@ -22,30 +22,30 @@ class TvpaintPrelaunchHook(PreLaunchHook): hosts = ["tvpaint"] def execute(self): + # Pop tvpaint executable tvpaint_executable = self.launch_context.launch_args.pop(0) - # This should never be used! + # Pop rest of launch arguments - There should not be other arguments! remainders = [] while self.launch_context.launch_args: remainders.append(self.launch_context.launch_args.pop(0)) - self.launch_context.launch_args.append( - self.main_executable() - ) - self.launch_context.launch_args.append( - "\"{}\"".format(self.launch_script_path()) - ) - self.launch_context.launch_args.append( + new_launch_args = [ + self.main_executable(), + "\"{}\"".format(self.launch_script_path()), "\"{}\"".format(tvpaint_executable) - ) + ] # Add workfile to launch arguments workfile_path = self.workfile_path() if workfile_path: - self.launch_context.launch_args.append( + new_launch_args.append( "\"{}\"".format(workfile_path) ) + # Append as whole list as these areguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + if remainders: self.log.warning(( "There are unexpected launch arguments in TVPaint launch. {}" From 9940ff2fa9ec5dfa2435f364b5543e726999bed7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 24 Nov 2020 20:02:18 +0100 Subject: [PATCH 22/54] added maya --- pype/hooks/maya/pre_launch_args.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 pype/hooks/maya/pre_launch_args.py diff --git a/pype/hooks/maya/pre_launch_args.py b/pype/hooks/maya/pre_launch_args.py new file mode 100644 index 0000000000..6aed54a3c3 --- /dev/null +++ b/pype/hooks/maya/pre_launch_args.py @@ -0,0 +1,17 @@ +import os +from pype.lib import PreLaunchHook + + +class MayaLaunchArguments(PreLaunchHook): + """Add path to last workfile to launch arguments.""" + order = 0 + hosts = ["maya"] + + def execute(self): + """Prepare suprocess launch arguments for NukeX.""" + if self.data.get("start_last_workfile"): + last_workfile = self.data.get("last_workfile_path") + if os.path.exists(last_workfile): + self.launch_context.launch_args.append( + "\"{}\"".format(last_workfile) + ) From 87d30338c4c0ebafd88bc9fd83a6f59b2ff74577 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 09:55:46 +0100 Subject: [PATCH 23/54] added harmony prelaunch --- pype/hooks/harmony/pre_launch_args.py | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 pype/hooks/harmony/pre_launch_args.py diff --git a/pype/hooks/harmony/pre_launch_args.py b/pype/hooks/harmony/pre_launch_args.py new file mode 100644 index 0000000000..7047e67616 --- /dev/null +++ b/pype/hooks/harmony/pre_launch_args.py @@ -0,0 +1,53 @@ +import os +import platform + +from pype.lib import PreLaunchHook + + +class HarmonyPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and execute python script of harmony + implementation before harmony executable. + """ + hosts = ["harmony"] + + def execute(self): + # Pop tvpaint executable + photoshop_executable = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + python_launch_args = [ + self.python_executable(), + "-c", + ( + "^\"import avalon.harmony;" + "avalon.harmony.launch(\"{}\")^\"\"" + ).format(photoshop_executable) + ] + if platform.system().lower() != "windows": + new_launch_args = python_launch_args + else: + new_launch_args = [ + "cmd.exe", + "/k", + "\"{}\"".format(" ".join(python_launch_args)) + ] + + # Append as whole list as these areguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.log.warning(( + "There are unexpected launch arguments in Harmony launch. {}" + ).format(str(remainders))) + self.launch_context.launch_args.extend(remainders) + + def python_executable(self): + """Should lead to python executable.""" + # TODO change in Pype 3 + return os.environ["PYPE_PYTHON_EXE"] From 7174dc16ef7f3e461902bb0590cbd90ce6f56411 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 10:16:03 +0100 Subject: [PATCH 24/54] added unreal prelaunch hook --- pype/hooks/unreal/pre_workfile_preparation.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 pype/hooks/unreal/pre_workfile_preparation.py diff --git a/pype/hooks/unreal/pre_workfile_preparation.py b/pype/hooks/unreal/pre_workfile_preparation.py new file mode 100644 index 0000000000..f0e09669dc --- /dev/null +++ b/pype/hooks/unreal/pre_workfile_preparation.py @@ -0,0 +1,95 @@ +import os + +from pype.lib import ( + PreLaunchHook, + ApplicationLaunchFailed +) +from pype.hosts.unreal import lib as unreal_lib + + +class UnrealPrelaunchHook(PreLaunchHook): + """ + 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): + asset_name = self.data["asset_name"] + task_name = self.data["task_name"] + workdir = self.env["AVALON_WORKDIR"] + engine_version = self.app_name.split("_")[-1] + unreal_project_name = f"{asset_name}_{task_name}" + + # Unreal is sensitive about project names longer then 20 chars + if len(unreal_project_name) > 20: + self.log.warning(( + f"Project name exceed 20 characters ({unreal_project_name})!" + )) + + # Unreal doesn't accept non alphabet characters at the start + # of the project name. This is because project name is then used + # in various places inside c++ code and there variable names cannot + # start with non-alpha. We append 'P' before project name to solve it. + # 😱 + if not unreal_project_name[:1].isalpha(): + self.log.warning(( + "Project name doesn't start with alphabet " + f"character ({unreal_project_name}). Appending 'P'" + )) + unreal_project_name = f"P{unreal_project_name}" + + project_path = os.path.join(workdir, unreal_project_name) + + self.log.info(( + f"{self.signature} requested UE4 version: " + f"[ {engine_version} ]" + )) + + detected = unreal_lib.get_engine_versions() + detected_str = ', '.join(detected.keys()) or 'none' + self.log.info(( + f"{self.signature} detected UE4 versions: " + f"[ {detected_str} ]" + )) + + engine_version = ".".join(engine_version.split(".")[:2]) + if engine_version not in detected.keys(): + raise ApplicationLaunchFailed(( + f"{self.signature} requested version not " + f"detected [ {engine_version} ]" + )) + + os.makedirs(project_path, exist_ok=True) + + project_file = os.path.join( + project_path, + f"{unreal_project_name}.uproject" + ) + if not os.path.isfile(project_file): + engine_path = detected[engine_version] + self.log.info(( + f"{self.signature} creating unreal " + f"project [ {unreal_project_name} ]" + )) + # Set "AVALON_UNREAL_PLUGIN" to current process environment for + # execution of `create_unreal_project` + env_key = "AVALON_UNREAL_PLUGIN" + if self.env.get(env_key): + os.environ[env_key] = self.env[env_key] + + unreal_lib.create_unreal_project( + unreal_project_name, + engine_version, + project_path, + engine_path=engine_path + ) + + # Append project file to launch arguments + self.launch_context.launch_args.append(f"\"{project_file}\"") From cc70fd75524a7fe2a34c8f6f6b0f9a50c28994d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 10:50:03 +0100 Subject: [PATCH 25/54] ftrack sync can use application manager for applications sync --- pype/modules/ftrack/lib/avalon_sync.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 7ff5283d6a..97116317af 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -17,7 +17,10 @@ from bson.errors import InvalidId from pymongo import UpdateOne import ftrack_api from pype.api import config - +from pype.lib import ( + ApplicationManager, + env_value_to_bool +) log = Logger().get_logger(__name__) @@ -186,12 +189,28 @@ def get_project_apps(in_app_list): dictionary of warnings """ apps = [] + warnings = collections.defaultdict(list) + + if env_value_to_bool("PYPE_USE_APP_MANAGER", default=False): + missing_app_msg = "Missing definition of application" + application_manager = ApplicationManager() + for app_name in in_app_list: + app = application_manager.applications.get(app_name) + if app: + apps.append({ + "name": app_name, + "label": app.full_label + }) + else: + warnings[missing_app_msg].append(app_name) + return apps, warnings + # TODO report missing_toml_msg = "Missing config file for application" error_msg = ( "Unexpected error happend during preparation of application" ) - warnings = collections.defaultdict(list) + for app in in_app_list: try: toml_path = avalon.lib.which_app(app) From 0de4a563eeef70bbc7b712f8872cbb491cb3eb9b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 10:50:26 +0100 Subject: [PATCH 26/54] added TVPaint to applications --- .../system_settings/global/applications.json | 49 +++++++++++++++++++ .../host_settings/schema_tvpaint.json | 41 ++++++++++++++++ .../system_schema/schema_applications.json | 4 ++ 3 files changed, 94 insertions(+) create mode 100644 pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_tvpaint.json diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json index 4bcea2fa30..d6ab2d7829 100644 --- a/pype/settings/defaults/system_settings/global/applications.json +++ b/pype/settings/defaults/system_settings/global/applications.json @@ -855,6 +855,55 @@ } } }, + "tvpaint": { + "enabled": true, + "label": "TVPaint", + "icon": "{}/app_icons/tvpaint.png", + "is_host": true, + "environment": { + "__environment_keys__": { + "tvpaint": [] + } + }, + "variants": { + "tvpaint_Animation 11 (64bits)": { + "enabled": true, + "label": "", + "variant_label": "Animation 11 (64bits)", + "icon": "", + "executables": { + "windows": [ + "C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe" + ], + "darwin": [], + "linux": [] + }, + "environment": { + "__environment_keys__": { + "tvpaint_Animation 11 (64bits)": [] + } + } + }, + "tvpaint_Animation 11 (32bits)": { + "enabled": true, + "label": "", + "variant_label": "Animation 11 (32bits)", + "icon": "", + "executables": { + "windows": [ + "C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe" + ], + "darwin": [], + "linux": [] + }, + "environment": { + "__environment_keys__": { + "tvpaint_Animation 11 (32bits)": [] + } + } + } + } + }, "photoshop": { "enabled": true, "label": "Adobe Photoshop", diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_tvpaint.json b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_tvpaint.json new file mode 100644 index 0000000000..09e5b1d907 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_tvpaint.json @@ -0,0 +1,41 @@ +{ + "type": "dict", + "key": "tvpaint", + "label": "TVPaint", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "schema_template", + "name": "template_host_unchangables" + }, + { + "key": "environment", + "label": "Environment", + "type": "raw-json", + "env_group_key": "tvpaint" + }, + { + "type": "dict-invisible", + "key": "variants", + "children": [{ + "type": "schema_template", + "name": "template_host_variant", + "template_data": [ + { + "host_version": "Animation 11 (64bits)", + "host_name": "tvpaint" + }, + { + "host_version": "Animation 11 (32bits)", + "host_name": "tvpaint" + } + ] + }] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/schema_applications.json b/pype/tools/settings/settings/gui_schemas/system_schema/schema_applications.json index ebfa4482bb..65cd16049e 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/schema_applications.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/schema_applications.json @@ -65,6 +65,10 @@ "type": "schema", "name": "schema_harmony" }, + { + "type": "schema", + "name": "schema_tvpaint" + }, { "type": "schema", "name": "schema_photoshop" From 9ff8366ca44f9d05a52b53af802ed28f0eb00a35 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 11:33:38 +0100 Subject: [PATCH 27/54] use different loading for python 3 --- pype/lib/python_module_tools.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pype/lib/python_module_tools.py b/pype/lib/python_module_tools.py index 61a3b1b09e..2ce2f60dca 100644 --- a/pype/lib/python_module_tools.py +++ b/pype/lib/python_module_tools.py @@ -1,9 +1,12 @@ import os +import sys import types +import importlib import inspect import logging log = logging.getLogger(__name__) +PY3 = sys.version_info[0] == 3 def modules_from_path(folder_path): @@ -39,11 +42,20 @@ def modules_from_path(folder_path): try: # Prepare module object where content of file will be parsed module = types.ModuleType(mod_name) - module.__file__ = full_path - with open(full_path) as _stream: - # Execute content and store it to module object - exec(_stream.read(), module.__dict__) + if PY3: + # Use loader so module has full specs + module_loader = importlib.machinery.SourceFileLoader( + mod_name, full_path + ) + module_loader.exec_module(module) + else: + # Execute module code and store content to module + with open(full_path) as _stream: + # Execute content and store it to module object + exec(_stream.read(), module.__dict__) + + module.__file__ = full_path modules.append(module) From 80a12c808e42de3ba281c1de83739734b13e6202 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 11:40:31 +0100 Subject: [PATCH 28/54] fix key error --- pype/hooks/celaction/pre_celaction_registers.py | 2 +- pype/hooks/tvpaint/pre_launch_args.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/hooks/celaction/pre_celaction_registers.py b/pype/hooks/celaction/pre_celaction_registers.py index c638ce3c0b..b65deda7f5 100644 --- a/pype/hooks/celaction/pre_celaction_registers.py +++ b/pype/hooks/celaction/pre_celaction_registers.py @@ -91,7 +91,7 @@ class CelactionPrelaunchHook(PreLaunchHook): winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) def workfile_path(self): - workfile_path = self.data["last_workfile"] + workfile_path = self.data["last_workfile_path"] # copy workfile from template if doesnt exist any on path if not os.path.exists(workfile_path): diff --git a/pype/hooks/tvpaint/pre_launch_args.py b/pype/hooks/tvpaint/pre_launch_args.py index 6c59a8ce20..1901041d94 100644 --- a/pype/hooks/tvpaint/pre_launch_args.py +++ b/pype/hooks/tvpaint/pre_launch_args.py @@ -67,7 +67,7 @@ class TvpaintPrelaunchHook(PreLaunchHook): return script_path def workfile_path(self): - workfile_path = self.data["last_workfile"] + workfile_path = self.data["last_workfile_path"] # copy workfile from template if doesnt exist any on path if not os.path.exists(workfile_path): From a40279d4567d0b2fff72e7b409a37902083d5154 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 11:50:45 +0100 Subject: [PATCH 29/54] fix minor clean arguments bug --- pype/lib/applications.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 9d34a8b22c..c1c6fc9301 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -1041,7 +1041,9 @@ class ApplicationLaunchContext: # Prepare subprocess args args = self.clear_launch_args(self.launch_args) self.log.debug( - "Launching \"{}\" with args: {}".format(self.app_name, args) + "Launching \"{}\" with args ({}): {}".format( + self.app_name, len(args), args + ) ) # Run process self.process = subprocess.Popen(args, **self.kwargs) @@ -1095,7 +1097,7 @@ class ApplicationLaunchContext: for _arg in arg: new_args.append(_arg) else: - new_args.append(args) + new_args.append(arg) args = new_args if all_cleared: From 92d34a0a66bc778f98e12192970f0c9dc92197e5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 13:18:51 +0100 Subject: [PATCH 30/54] fixed tvpaint launch --- pype/hooks/tvpaint/pre_launch_args.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pype/hooks/tvpaint/pre_launch_args.py b/pype/hooks/tvpaint/pre_launch_args.py index 1901041d94..60ddcb6167 100644 --- a/pype/hooks/tvpaint/pre_launch_args.py +++ b/pype/hooks/tvpaint/pre_launch_args.py @@ -32,7 +32,7 @@ class TvpaintPrelaunchHook(PreLaunchHook): new_launch_args = [ self.main_executable(), - "\"{}\"".format(self.launch_script_path()), + self.launch_script_path(), "\"{}\"".format(tvpaint_executable) ] @@ -43,6 +43,15 @@ class TvpaintPrelaunchHook(PreLaunchHook): "\"{}\"".format(workfile_path) ) + # How to create new command line + # if platform.system().lower() == "windows": + # new_launch_args = [ + # "cmd.exe", + # "/c", + # "Call cmd.exe /k", + # *new_launch_args + # ] + # Append as whole list as these areguments should not be separated self.launch_context.launch_args.append(new_launch_args) @@ -55,7 +64,7 @@ class TvpaintPrelaunchHook(PreLaunchHook): def main_executable(self): """Should lead to python executable.""" # TODO change in Pype 3 - return os.environ["PYPE_PYTHON_EXE"] + return os.path.normpath(os.environ["PYPE_PYTHON_EXE"]) def launch_script_path(self): avalon_dir = os.path.dirname(os.path.abspath(avalon.__file__)) From 674f287fe17f476991dcfff4fd718385366314d0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 13:53:08 +0100 Subject: [PATCH 31/54] change harmony and photoshop args --- pype/hooks/harmony/pre_launch_args.py | 16 +++++++--------- pype/hooks/photoshop/pre_launch_args.py | 17 ++++++++--------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/pype/hooks/harmony/pre_launch_args.py b/pype/hooks/harmony/pre_launch_args.py index 7047e67616..eabe1e6f87 100644 --- a/pype/hooks/harmony/pre_launch_args.py +++ b/pype/hooks/harmony/pre_launch_args.py @@ -21,7 +21,7 @@ class HarmonyPrelaunchHook(PreLaunchHook): while self.launch_context.launch_args: remainders.append(self.launch_context.launch_args.pop(0)) - python_launch_args = [ + new_launch_args = [ self.python_executable(), "-c", ( @@ -29,14 +29,12 @@ class HarmonyPrelaunchHook(PreLaunchHook): "avalon.harmony.launch(\"{}\")^\"\"" ).format(photoshop_executable) ] - if platform.system().lower() != "windows": - new_launch_args = python_launch_args - else: - new_launch_args = [ - "cmd.exe", - "/k", - "\"{}\"".format(" ".join(python_launch_args)) - ] + # if platform.system().lower() == "windows": + # new_launch_args = [ + # "cmd.exe", + # "/k", + # "\"{}\"".format(" ".join(new_launch_args)) + # ] # Append as whole list as these areguments should not be separated self.launch_context.launch_args.append(new_launch_args) diff --git a/pype/hooks/photoshop/pre_launch_args.py b/pype/hooks/photoshop/pre_launch_args.py index bc747dc495..77449a8269 100644 --- a/pype/hooks/photoshop/pre_launch_args.py +++ b/pype/hooks/photoshop/pre_launch_args.py @@ -21,7 +21,7 @@ class PhotoshopPrelaunchHook(PreLaunchHook): while self.launch_context.launch_args: remainders.append(self.launch_context.launch_args.pop(0)) - python_launch_args = [ + new_launch_args = [ self.python_executable(), "-c", ( @@ -29,14 +29,13 @@ class PhotoshopPrelaunchHook(PreLaunchHook): "avalon.photoshop.launch(\"{}\")^\"\"" ).format(photoshop_executable) ] - if platform.system().lower() != "windows": - new_launch_args = python_launch_args - else: - new_launch_args = [ - "cmd.exe", - "/k", - "\"{}\"".format(" ".join(python_launch_args)) - ] + + # if platform.system().lower() == "windows": + # new_launch_args = [ + # "cmd.exe", + # "/k", + # "\"{}\"".format(" ".join(new_launch_args)) + # ] # Append as whole list as these areguments should not be separated self.launch_context.launch_args.append(new_launch_args) From e1c9cd637bd92b867394fc1a031c138e4680b00c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 14:55:27 +0100 Subject: [PATCH 32/54] fix launch args --- pype/hooks/tvpaint/pre_launch_args.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hooks/tvpaint/pre_launch_args.py b/pype/hooks/tvpaint/pre_launch_args.py index 60ddcb6167..7a5acb08fa 100644 --- a/pype/hooks/tvpaint/pre_launch_args.py +++ b/pype/hooks/tvpaint/pre_launch_args.py @@ -33,7 +33,7 @@ class TvpaintPrelaunchHook(PreLaunchHook): new_launch_args = [ self.main_executable(), self.launch_script_path(), - "\"{}\"".format(tvpaint_executable) + tvpaint_executable ] # Add workfile to launch arguments From 7e68d5b6498daa93c92025244fc6a7d80381bb39 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 15:49:23 +0100 Subject: [PATCH 33/54] hound cleanups --- pype/hooks/harmony/pre_launch_args.py | 1 - pype/hooks/photoshop/pre_launch_args.py | 1 - pype/hooks/tvpaint/pre_install_pywin.py | 3 ++- pype/hooks/tvpaint/pre_launch_args.py | 7 ++----- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pype/hooks/harmony/pre_launch_args.py b/pype/hooks/harmony/pre_launch_args.py index eabe1e6f87..cafb2a4c60 100644 --- a/pype/hooks/harmony/pre_launch_args.py +++ b/pype/hooks/harmony/pre_launch_args.py @@ -1,5 +1,4 @@ import os -import platform from pype.lib import PreLaunchHook diff --git a/pype/hooks/photoshop/pre_launch_args.py b/pype/hooks/photoshop/pre_launch_args.py index 77449a8269..5b326f882b 100644 --- a/pype/hooks/photoshop/pre_launch_args.py +++ b/pype/hooks/photoshop/pre_launch_args.py @@ -1,5 +1,4 @@ import os -import platform from pype.lib import PreLaunchHook diff --git a/pype/hooks/tvpaint/pre_install_pywin.py b/pype/hooks/tvpaint/pre_install_pywin.py index 20d4b2aae7..b51267934b 100644 --- a/pype/hooks/tvpaint/pre_install_pywin.py +++ b/pype/hooks/tvpaint/pre_install_pywin.py @@ -26,8 +26,9 @@ class PreInstallPyWin(PreLaunchHook): try: output = _subprocess( - ["pip", "install", "pywin32==227"], logger=self.log + ["pip", "install", "pywin32==227"] ) + self.log.debug("Pip install pywin32 output:\n{}'".format(output)) except RuntimeError: msg = "Installation of python module `pywin32` crashed." self.log.warning(msg, exc_info=True) diff --git a/pype/hooks/tvpaint/pre_launch_args.py b/pype/hooks/tvpaint/pre_launch_args.py index 7a5acb08fa..9d80e6fddb 100644 --- a/pype/hooks/tvpaint/pre_launch_args.py +++ b/pype/hooks/tvpaint/pre_launch_args.py @@ -2,11 +2,8 @@ import os import shutil from pype.hosts import tvpaint -from pype.lib import ( - PreLaunchHook, - ApplicationLaunchFailed, - _subprocess -) +from pype.lib import PreLaunchHook + import avalon From c77946c181838c8dfa99b932ecd256f86967ad08 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 15:52:15 +0100 Subject: [PATCH 34/54] added aftereffects prelaunch hook --- pype/hooks/aftereffects/pre_launch_args.py | 45 ++++++++++++++++++++++ pype/hooks/harmony/pre_launch_args.py | 6 --- pype/hooks/photoshop/pre_launch_args.py | 7 ---- 3 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 pype/hooks/aftereffects/pre_launch_args.py diff --git a/pype/hooks/aftereffects/pre_launch_args.py b/pype/hooks/aftereffects/pre_launch_args.py new file mode 100644 index 0000000000..9bd3a41ee4 --- /dev/null +++ b/pype/hooks/aftereffects/pre_launch_args.py @@ -0,0 +1,45 @@ +import os + +from pype.lib import PreLaunchHook + + +class AfterEffectsPrelaunchHook(PreLaunchHook): + """Launch arguments preparation. + + Hook add python executable and execute python script of AfterEffects + implementation before AfterEffects executable. + """ + hosts = ["aftereffects"] + + def execute(self): + # Pop tvpaint executable + photoshop_executable = self.launch_context.launch_args.pop(0) + + # Pop rest of launch arguments - There should not be other arguments! + remainders = [] + while self.launch_context.launch_args: + remainders.append(self.launch_context.launch_args.pop(0)) + + new_launch_args = [ + self.python_executable(), + "-c", + ( + "^\"import avalon.aftereffects;" + "avalon.aftereffects.launch(\"{}\")^\"\"" + ).format(photoshop_executable) + ] + + # Append as whole list as these areguments should not be separated + self.launch_context.launch_args.append(new_launch_args) + + if remainders: + self.log.warning(( + "There are unexpected launch arguments " + "in AfterEffects launch. {}" + ).format(str(remainders))) + self.launch_context.launch_args.extend(remainders) + + def python_executable(self): + """Should lead to python executable.""" + # TODO change in Pype 3 + return os.environ["PYPE_PYTHON_EXE"] diff --git a/pype/hooks/harmony/pre_launch_args.py b/pype/hooks/harmony/pre_launch_args.py index cafb2a4c60..48a72d6ec9 100644 --- a/pype/hooks/harmony/pre_launch_args.py +++ b/pype/hooks/harmony/pre_launch_args.py @@ -28,12 +28,6 @@ class HarmonyPrelaunchHook(PreLaunchHook): "avalon.harmony.launch(\"{}\")^\"\"" ).format(photoshop_executable) ] - # if platform.system().lower() == "windows": - # new_launch_args = [ - # "cmd.exe", - # "/k", - # "\"{}\"".format(" ".join(new_launch_args)) - # ] # Append as whole list as these areguments should not be separated self.launch_context.launch_args.append(new_launch_args) diff --git a/pype/hooks/photoshop/pre_launch_args.py b/pype/hooks/photoshop/pre_launch_args.py index 5b326f882b..840e4e2a32 100644 --- a/pype/hooks/photoshop/pre_launch_args.py +++ b/pype/hooks/photoshop/pre_launch_args.py @@ -29,13 +29,6 @@ class PhotoshopPrelaunchHook(PreLaunchHook): ).format(photoshop_executable) ] - # if platform.system().lower() == "windows": - # new_launch_args = [ - # "cmd.exe", - # "/k", - # "\"{}\"".format(" ".join(new_launch_args)) - # ] - # Append as whole list as these areguments should not be separated self.launch_context.launch_args.append(new_launch_args) From 576c359b4ff8de3445babbcf46c1f6817580268d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 15:56:55 +0100 Subject: [PATCH 35/54] added aftereffects to application schemas --- .../system_settings/global/applications.json | 67 ++++++++++++++++++- .../host_settings/schema_aftereffects.json | 41 ++++++++++++ .../system_schema/schema_applications.json | 4 ++ 3 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_aftereffects.json diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json index d6ab2d7829..3eda5e036d 100644 --- a/pype/settings/defaults/system_settings/global/applications.json +++ b/pype/settings/defaults/system_settings/global/applications.json @@ -862,8 +862,11 @@ "is_host": true, "environment": { "__environment_keys__": { - "tvpaint": [] - } + "tvpaint": [ + "PYPE_LOG_NO_COLORS" + ] + }, + "PYPE_LOG_NO_COLORS": "True" }, "variants": { "tvpaint_Animation 11 (64bits)": { @@ -944,6 +947,66 @@ } } }, + "aftereffects": { + "enabled": true, + "label": "Adobe AfterEffects", + "icon": "{}/app_icons/aftereffects.png", + "is_host": true, + "environment": { + "__environment_keys__": { + "aftereffects": [ + "AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH", + "PYTHONPATH", + "PYPE_LOG_NO_COLORS", + "WEBSOCKET_URL", + "WORKFILES_SAVE_AS" + ] + }, + "AVALON_AFTEREFFECTS_WORKFILES_ON_LAUNCH": "1", + "PYTHONPATH": "{PYTHONPATH}", + "PYPE_LOG_NO_COLORS": "Yes", + "WEBSOCKET_URL": "ws://localhost:8097/ws/", + "WORKFILES_SAVE_AS": "Yes" + }, + "variants": { + "aftereffects_2020": { + "enabled": true, + "label": "", + "variant_label": "2020", + "icon": "", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2020\\Support Files\\AfterFX.exe" + ], + "darwin": [], + "linux": [] + }, + "environment": { + "__environment_keys__": { + "aftereffects_2020": [] + } + } + }, + "aftereffects_2021": { + "enabled": true, + "label": "", + "variant_label": "2021", + "icon": "", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe" + ], + "darwin": [], + "linux": [] + }, + "environment": { + "__environment_keys__": { + "aftereffects_2021": [] + } + } + } + } + }, "celaction": { "enabled": true, "label": "CelAction 2D", diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_aftereffects.json b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_aftereffects.json new file mode 100644 index 0000000000..073d57b870 --- /dev/null +++ b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_aftereffects.json @@ -0,0 +1,41 @@ +{ + "type": "dict", + "key": "aftereffects", + "label": "Adobe AfterEffects", + "collapsable": true, + "checkbox_key": "enabled", + "children": [{ + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "schema_template", + "name": "template_host_unchangables" + }, + { + "key": "environment", + "label": "Environment", + "type": "raw-json", + "env_group_key": "aftereffects" + }, + { + "type": "dict-invisible", + "key": "variants", + "children": [{ + "type": "schema_template", + "name": "template_host_variant", + "template_data": [ + { + "host_version": "2020", + "host_name": "aftereffects" + }, + { + "host_version": "2021", + "host_name": "aftereffects" + } + ] + }] + } + ] +} diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/schema_applications.json b/pype/tools/settings/settings/gui_schemas/system_schema/schema_applications.json index 65cd16049e..1c983bcff2 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/schema_applications.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/schema_applications.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_photoshop" }, + { + "type": "schema", + "name": "schema_aftereffects" + }, { "type": "schema", "name": "schema_celaction" From 8f08412ec7f2129298b0409df417e27ba4b46e9d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 17:04:07 +0100 Subject: [PATCH 36/54] fixed python launch --- pype/hooks/aftereffects/pre_launch_args.py | 8 ++++---- pype/hooks/harmony/pre_launch_args.py | 8 ++++---- pype/hooks/photoshop/pre_launch_args.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pype/hooks/aftereffects/pre_launch_args.py b/pype/hooks/aftereffects/pre_launch_args.py index 9bd3a41ee4..01406d495c 100644 --- a/pype/hooks/aftereffects/pre_launch_args.py +++ b/pype/hooks/aftereffects/pre_launch_args.py @@ -13,7 +13,7 @@ class AfterEffectsPrelaunchHook(PreLaunchHook): def execute(self): # Pop tvpaint executable - photoshop_executable = self.launch_context.launch_args.pop(0) + aftereffects_executable = self.launch_context.launch_args.pop(0) # Pop rest of launch arguments - There should not be other arguments! remainders = [] @@ -24,9 +24,9 @@ class AfterEffectsPrelaunchHook(PreLaunchHook): self.python_executable(), "-c", ( - "^\"import avalon.aftereffects;" - "avalon.aftereffects.launch(\"{}\")^\"\"" - ).format(photoshop_executable) + "import avalon.aftereffects;" + "avalon.aftereffects.launch(\"{}\")" + ).format(aftereffects_executable) ] # Append as whole list as these areguments should not be separated diff --git a/pype/hooks/harmony/pre_launch_args.py b/pype/hooks/harmony/pre_launch_args.py index 48a72d6ec9..70fac5bb76 100644 --- a/pype/hooks/harmony/pre_launch_args.py +++ b/pype/hooks/harmony/pre_launch_args.py @@ -13,7 +13,7 @@ class HarmonyPrelaunchHook(PreLaunchHook): def execute(self): # Pop tvpaint executable - photoshop_executable = self.launch_context.launch_args.pop(0) + harmony_executable = self.launch_context.launch_args.pop(0) # Pop rest of launch arguments - There should not be other arguments! remainders = [] @@ -24,9 +24,9 @@ class HarmonyPrelaunchHook(PreLaunchHook): self.python_executable(), "-c", ( - "^\"import avalon.harmony;" - "avalon.harmony.launch(\"{}\")^\"\"" - ).format(photoshop_executable) + "import avalon.harmony;" + "avalon.harmony.launch(\"{}\")" + ).format(harmony_executable) ] # Append as whole list as these areguments should not be separated diff --git a/pype/hooks/photoshop/pre_launch_args.py b/pype/hooks/photoshop/pre_launch_args.py index 840e4e2a32..2c88f62157 100644 --- a/pype/hooks/photoshop/pre_launch_args.py +++ b/pype/hooks/photoshop/pre_launch_args.py @@ -24,8 +24,8 @@ class PhotoshopPrelaunchHook(PreLaunchHook): self.python_executable(), "-c", ( - "^\"import avalon.photoshop;" - "avalon.photoshop.launch(\"{}\")^\"\"" + "import avalon.photoshop;" + "avalon.photoshop.launch(\"{}\")" ).format(photoshop_executable) ] From ec0127c79a17fcf51a87c0d4658d744084ed3f01 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 25 Nov 2020 17:06:10 +0100 Subject: [PATCH 37/54] added photoshop 2021 --- .../system_settings/global/applications.json | 22 ++++++++++++++++++- .../host_settings/schema_photoshop.json | 4 ++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json index 3eda5e036d..c638f1e3d2 100644 --- a/pype/settings/defaults/system_settings/global/applications.json +++ b/pype/settings/defaults/system_settings/global/applications.json @@ -935,7 +935,9 @@ "variant_label": "2020", "icon": "", "executables": { - "windows": [], + "windows": [ + "C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe" + ], "darwin": [], "linux": [] }, @@ -944,6 +946,24 @@ "photoshop_2020": [] } } + }, + "photoshop_2021": { + "enabled": true, + "label": "", + "variant_label": "2021", + "icon": "", + "executables": { + "windows": [ + "C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe" + ], + "darwin": [], + "linux": [] + }, + "environment": { + "__environment_keys__": { + "photoshop_2021": [] + } + } } } }, diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_photoshop.json b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_photoshop.json index f86f6ff055..0c27611b29 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_photoshop.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_photoshop.json @@ -29,6 +29,10 @@ { "host_version": "2020", "host_name": "photoshop" + }, + { + "host_version": "2021", + "host_name": "photoshop" } ] }] From 6a4c768f21182ed2e0a0584311dc4ca3cc6b4b98 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 10:06:48 +0100 Subject: [PATCH 38/54] there are not added nuke arguments in prelaunch hooks --- pype/hooks/hiero/pre_launch_args.py | 14 ++------------ pype/hooks/maya/pre_launch_args.py | 3 ++- pype/hooks/nukestudio/pre_launch_args.py | 14 ++------------ pype/hooks/nukex/pre_launch_args.py | 12 +----------- 4 files changed, 7 insertions(+), 36 deletions(-) diff --git a/pype/hooks/hiero/pre_launch_args.py b/pype/hooks/hiero/pre_launch_args.py index cb03f03b88..7139f57bbb 100644 --- a/pype/hooks/hiero/pre_launch_args.py +++ b/pype/hooks/hiero/pre_launch_args.py @@ -7,18 +7,8 @@ class HieroLaunchArguments(PreLaunchHook): hosts = ["hiero"] def execute(self): - """Prepare suprocess launch arguments for NukeX.""" - # Get executable - executable = self.launch_context.launch_args[0] - - if isinstance(executable, str): - executable = [executable] - - # Add `nukex` argument and make sure it's bind to execuable - executable.append("--hiero") - - self.launch_context.launch_args[0] = executable - + """Prepare suprocess launch arguments for Hiero.""" + # Add path to workfile to arguments if self.data.get("start_last_workfile"): last_workfile = self.data.get("last_workfile_path") if os.path.exists(last_workfile): diff --git a/pype/hooks/maya/pre_launch_args.py b/pype/hooks/maya/pre_launch_args.py index 6aed54a3c3..570c8e79b8 100644 --- a/pype/hooks/maya/pre_launch_args.py +++ b/pype/hooks/maya/pre_launch_args.py @@ -8,7 +8,8 @@ class MayaLaunchArguments(PreLaunchHook): hosts = ["maya"] def execute(self): - """Prepare suprocess launch arguments for NukeX.""" + """Prepare suprocess launch arguments for Maya.""" + # Add path to workfile to arguments if self.data.get("start_last_workfile"): last_workfile = self.data.get("last_workfile_path") if os.path.exists(last_workfile): diff --git a/pype/hooks/nukestudio/pre_launch_args.py b/pype/hooks/nukestudio/pre_launch_args.py index d567f36ad0..a5e04bf956 100644 --- a/pype/hooks/nukestudio/pre_launch_args.py +++ b/pype/hooks/nukestudio/pre_launch_args.py @@ -7,18 +7,8 @@ class NukeStudioLaunchArguments(PreLaunchHook): hosts = ["nukestudio"] def execute(self): - """Prepare suprocess launch arguments for NukeX.""" - # Get executable - executable = self.launch_context.launch_args[0] - - if isinstance(executable, str): - executable = [executable] - - # Add `nukex` argument and make sure it's bind to execuable - executable.append("--studio") - - self.launch_context.launch_args[0] = executable - + """Prepare suprocess launch arguments for NukeStudio.""" + # Add path to workfile to arguments if self.data.get("start_last_workfile"): last_workfile = self.data.get("last_workfile_path") if os.path.exists(last_workfile): diff --git a/pype/hooks/nukex/pre_launch_args.py b/pype/hooks/nukex/pre_launch_args.py index eb0b963926..39ccb5a58a 100644 --- a/pype/hooks/nukex/pre_launch_args.py +++ b/pype/hooks/nukex/pre_launch_args.py @@ -8,17 +8,7 @@ class NukeXLaunchArguments(PreLaunchHook): def execute(self): """Prepare suprocess launch arguments for NukeX.""" - # Get executable - executable = self.launch_context.launch_args[0] - - if isinstance(executable, str): - executable = [executable] - - # Add `nukex` argument and make sure it's bind to execuable - executable.append("--nukex") - - self.launch_context.launch_args[0] = executable - + # Add path to workfile to arguments if self.data.get("start_last_workfile"): last_workfile = self.data.get("last_workfile_path") if os.path.exists(last_workfile): From cf00c0c57729aca8328a9216a37fea718eb4fcff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 10:06:56 +0100 Subject: [PATCH 39/54] added resolve prelaunch hook --- .../celaction/pre_celaction_registers.py | 3 +- pype/hooks/resolve/pre_resolve_setup.py | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 pype/hooks/resolve/pre_resolve_setup.py diff --git a/pype/hooks/celaction/pre_celaction_registers.py b/pype/hooks/celaction/pre_celaction_registers.py index b65deda7f5..f592026699 100644 --- a/pype/hooks/celaction/pre_celaction_registers.py +++ b/pype/hooks/celaction/pre_celaction_registers.py @@ -41,8 +41,7 @@ class CelactionPrelaunchHook(PreLaunchHook): # TODO: change to root path and pyblish standalone to premiere way pype_root_path = os.getenv("PYPE_SETUP_PATH") - path = os.path.join(pype_root_path, - "pype.bat") + path = os.path.join(pype_root_path, "pype.bat") winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, path) diff --git a/pype/hooks/resolve/pre_resolve_setup.py b/pype/hooks/resolve/pre_resolve_setup.py new file mode 100644 index 0000000000..336fabf0c4 --- /dev/null +++ b/pype/hooks/resolve/pre_resolve_setup.py @@ -0,0 +1,58 @@ +import os +import importlib +from pype.lib import PreLaunchHook +from pype.hosts.resolve import utils + + +class ResolvePrelaunch(PreLaunchHook): + """ + This hook will check if current workfile path has Resolve + project inside. IF not, it initialize it and finally it pass + path to the project by environment variable to Premiere launcher + shell script. + """ + hosts = ["resolve"] + + def execute(self): + # making sure pyton 3.6 is installed at provided path + py36_dir = os.path.normpath(self.env.get("PYTHON36_RESOLVE", "")) + assert os.path.isdir(py36_dir), ( + "Python 3.6 is not installed at the provided folder path. Either " + "make sure the `environments\resolve.json` is having correctly " + "set `PYTHON36_RESOLVE` or make sure Python 3.6 is installed " + f"in given path. \nPYTHON36_RESOLVE: `{py36_dir}`" + ) + self.log.info(f"Path to Resolve Python folder: `{py36_dir}`...") + self.env["PYTHON36_RESOLVE"] = py36_dir + + # setting utility scripts dir for scripts syncing + us_dir = os.path.normpath( + self.env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") + ) + assert os.path.isdir(us_dir), ( + "Resolve utility script dir does not exists. Either make sure " + "the `environments\resolve.json` is having correctly set " + "`RESOLVE_UTILITY_SCRIPTS_DIR` or reinstall DaVinci Resolve. \n" + f"RESOLVE_UTILITY_SCRIPTS_DIR: `{us_dir}`" + ) + self.log.debug(f"-- us_dir: `{us_dir}`") + + # correctly format path for pre python script + pre_py_sc = os.path.normpath(self.env.get("PRE_PYTHON_SCRIPT", "")) + self.env["PRE_PYTHON_SCRIPT"] = pre_py_sc + self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...") + try: + __import__("pype.hosts.resolve") + __import__("pyblish") + + except ImportError: + self.log.warning( + "pyblish: Could not load Resolve integration.", + exc_info=True + ) + + else: + # Resolve Setup integration + importlib.reload(utils) + self.log.debug(f"-- utils.__file__: `{utils.__file__}`") + utils.setup(self.env) From 819998f068989614839f629ee4dfecf47aa37ce3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 10:34:11 +0100 Subject: [PATCH 40/54] replaced `is_host` key with `host_name` --- .../system_settings/global/applications.json | 34 +++++++++---------- .../template_host_unchangables.json | 22 ++++++++++-- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json index c638f1e3d2..7dd0057110 100644 --- a/pype/settings/defaults/system_settings/global/applications.json +++ b/pype/settings/defaults/system_settings/global/applications.json @@ -3,7 +3,7 @@ "enabled": true, "label": "Autodesk Maya", "icon": "{}/app_icons/maya.png", - "is_host": true, + "host_name": "maya", "environment": { "__environment_keys__": { "maya": [ @@ -107,7 +107,7 @@ "enabled": true, "label": "Autodesk MayaBatch", "icon": "{}/app_icons/maya.png", - "is_host": false, + "host_name": "maya", "environment": { "__environment_keys__": { "mayabatch": [ @@ -205,7 +205,7 @@ "enabled": true, "label": "Nuke", "icon": "{}/app_icons/nuke.png", - "is_host": true, + "host_name": "nuke", "environment": { "__environment_keys__": { "nuke": [ @@ -287,7 +287,7 @@ "enabled": true, "label": "Nuke X", "icon": "{}/app_icons/nuke.png", - "is_host": true, + "host_name": "nuke", "environment": { "__environment_keys__": { "nukex": [ @@ -369,7 +369,7 @@ "enabled": true, "label": "Nuke Studio", "icon": "{}/app_icons/nuke.png", - "is_host": true, + "host_name": "hiero", "environment": { "__environment_keys__": { "nukestudio": [ @@ -453,7 +453,7 @@ "enabled": true, "label": "Hiero", "icon": "{}/app_icons/hiero.png", - "is_host": true, + "host_name": "hiero", "environment": { "__environment_keys__": { "hiero": [ @@ -539,7 +539,7 @@ "enabled": true, "label": "BlackMagic Fusion", "icon": "{}/app_icons/fusion.png", - "is_host": true, + "host_name": "fusion", "environment": { "__environment_keys__": { "fusion": [] @@ -584,7 +584,7 @@ "enabled": true, "label": "Blackmagic DaVinci Resolve", "icon": "{}/app_icons/resolve.png", - "is_host": true, + "host_name": "resolve", "environment": { "__environment_keys__": { "resolve": [ @@ -662,7 +662,7 @@ "enabled": true, "label": "SideFX Houdini", "icon": "{}/app_icons/houdini.png", - "is_host": true, + "host_name": "houdini", "environment": { "__environment_keys__": { "houdini": [ @@ -720,7 +720,7 @@ "enabled": true, "label": "Blender", "icon": "{}/app_icons/blender.png", - "is_host": true, + "host_name": "blender", "environment": { "__environment_keys__": { "blender": [ @@ -775,7 +775,7 @@ "enabled": true, "label": "Toon Boom Harmony", "icon": "{}/app_icons/harmony.png", - "is_host": true, + "host_name": "harmony", "environment": { "__environment_keys__": { "harmony": [ @@ -859,7 +859,7 @@ "enabled": true, "label": "TVPaint", "icon": "{}/app_icons/tvpaint.png", - "is_host": true, + "host_name": "tvpaint", "environment": { "__environment_keys__": { "tvpaint": [ @@ -911,7 +911,7 @@ "enabled": true, "label": "Adobe Photoshop", "icon": "{}/app_icons/photoshop.png", - "is_host": true, + "host_name": "photoshop", "environment": { "__environment_keys__": { "photoshop": [ @@ -971,7 +971,7 @@ "enabled": true, "label": "Adobe AfterEffects", "icon": "{}/app_icons/aftereffects.png", - "is_host": true, + "host_name": "aftereffects", "environment": { "__environment_keys__": { "aftereffects": [ @@ -1031,7 +1031,7 @@ "enabled": true, "label": "CelAction 2D", "icon": "app_icons/celaction.png", - "is_host": true, + "host_name": "celaction", "environment": { "__environment_keys__": { "celaction": [ @@ -1071,7 +1071,7 @@ "enabled": true, "label": "Unreal Editor", "icon": "{}/app_icons/ue4.png'", - "is_host": true, + "host_name": "unreal", "environment": { "__environment_keys__": { "unreal": [ @@ -1165,7 +1165,7 @@ "enabled": true, "label": "DJV View", "icon": "{}/app_icons/djvView.png", - "is_host": false, + "host_name": "", "environment": { "__environment_keys__": { "djvview": [] diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/template_host_unchangables.json b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/template_host_unchangables.json index 732fd06c30..5fde8e9c1e 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/template_host_unchangables.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/template_host_unchangables.json @@ -13,8 +13,24 @@ "roles": ["developer"] }, { - "type": "boolean", - "key": "is_host", - "label": "Has host implementation", + "type": "enum", + "key": "host_name", + "label": "Host implementation", + "enum_items": [ + {"": "< without host >"}, + {"aftereffects": "aftereffects"}, + {"blender": "blender"}, + {"celaction": "celaction"}, + {"fusion": "fusion"}, + {"harmony": "harmony"}, + {"hiero": "hiero"}, + {"houdini": "houdini"}, + {"maya": "maya"}, + {"nuke": "nuke"}, + {"photoshop": "photoshop"}, + {"resolve": "resolve"}, + {"tvpaint": "tvpaint"}, + {"unreal": "unreal"} + ], "roles": ["developer"] }] From 3393933e28071959b02f1702d0a53a652d71d9db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 10:35:54 +0100 Subject: [PATCH 41/54] application manager load app_group and host_name --- pype/lib/applications.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index c1c6fc9301..37eb4f9e71 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -530,12 +530,12 @@ class ApplicationManager: settings = system_settings() hosts_definitions = settings["global"]["applications"] - for host_name, variant_definitions in hosts_definitions.items(): + for app_group, variant_definitions in hosts_definitions.items(): enabled = variant_definitions["enabled"] - label = variant_definitions.get("label") or host_name + label = variant_definitions.get("label") or app_group variants = variant_definitions.get("variants") or {} icon = variant_definitions.get("icon") - is_host = variant_definitions.get("is_host", False) + host_name = variant_definitions.get("host_name") or None for app_name, app_data in variants.items(): if app_name in self.applications: raise AssertionError(( @@ -553,11 +553,10 @@ class ApplicationManager: if not app_data.get("icon"): app_data["icon"] = icon - is_host = app_data.get("is_host", is_host) - app_data["is_host"] = is_host + app_data["is_host"] = host_name is not None self.applications[app_name] = Application( - host_name, app_name, app_data, self + app_group, app_name, host_name, app_data, self ) tools_definitions = settings["global"]["tools"] @@ -635,19 +634,21 @@ class Application: Object by itself does nothing special. Args: - host_name (str): Host name or rather name of host implementation. + app_group (str): App group name. e.g. "maya", "nuke", "photoshop", etc. app_name (str): Specific version (or variant) of host. e.g. "maya2020", "nuke11.3", etc. + host_name (str): Name of host implementation. app_data (dict): Data for the version containing information about executables, label, variant label, icon or if is enabled. Only required key is `executables`. manager (ApplicationManager): Application manager that created object. """ - def __init__(self, host_name, app_name, app_data, manager): - self.host_name = host_name + def __init__(self, app_group, app_name, host_name, app_data, manager): + self.app_group = app_group self.app_name = app_name + self.host_name = host_name self.app_data = app_data self.manager = manager @@ -768,6 +769,10 @@ class LaunchHook: def host_name(self): return getattr(self.application, "host_name", None) + @property + def app_group(self): + return getattr(self.application, "app_group", None) + @property def app_name(self): return getattr(self.application, "app_name", None) @@ -1010,6 +1015,10 @@ class ApplicationLaunchContext: def host_name(self): return self.application.host_name + @property + def app_group(self): + return self.application.app_group + @property def manager(self): return self.application.manager From c8c8424cb7ac4689fea14e140133e172f29907f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 10:37:52 +0100 Subject: [PATCH 42/54] hooks filtering works for app_groups and app_names --- pype/lib/applications.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 37eb4f9e71..ac45112511 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -710,6 +710,10 @@ class LaunchHook: order = None # List of host implementations, skipped if empty. hosts = [] + # List of application groups + app_groups = [] + # List of specific application names + app_names = [] # List of platform availability, skipped if empty. platforms = [] @@ -751,6 +755,14 @@ class LaunchHook: if launch_context.host_name not in cls.hosts: return False + if cls.app_groups: + if launch_context.app_group not in cls.app_groups: + return False + + if cls.app_names: + if launch_context.app_name not in cls.app_names: + return False + return True @property From f8868d04bca8bd4951f0ee8958a668c11dbeaafd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 10:39:17 +0100 Subject: [PATCH 43/54] environments are per app group not per host name --- pype/hooks/global/pre_global_host_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hooks/global/pre_global_host_data.py b/pype/hooks/global/pre_global_host_data.py index 787460019d..3f403b43f5 100644 --- a/pype/hooks/global/pre_global_host_data.py +++ b/pype/hooks/global/pre_global_host_data.py @@ -86,7 +86,7 @@ class GlobalHostDataHook(PreLaunchHook): def prepare_host_environments(self): """Modify launch environments based on launched app and context.""" # Keys for getting environments - env_keys = [self.host_name, self.app_name] + env_keys = [self.app_group, self.app_name] asset_doc = self.data.get("asset_doc") if asset_doc: From 19e5ab8b1a28120d6b31d43b4310cfa6495bebc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 10:40:21 +0100 Subject: [PATCH 44/54] changed current filtering from hosts to app groups --- pype/hooks/aftereffects/pre_launch_args.py | 2 +- pype/hooks/celaction/pre_celaction_registers.py | 2 +- pype/hooks/fusion/pre_fusion_setup.py | 2 +- pype/hooks/harmony/pre_launch_args.py | 2 +- pype/hooks/hiero/pre_launch_args.py | 2 +- pype/hooks/maya/pre_launch_args.py | 2 +- pype/hooks/nukestudio/pre_launch_args.py | 2 +- pype/hooks/nukex/pre_launch_args.py | 2 +- pype/hooks/photoshop/pre_launch_args.py | 2 +- pype/hooks/resolve/pre_resolve_setup.py | 2 +- pype/hooks/tvpaint/pre_install_pywin.py | 2 +- pype/hooks/tvpaint/pre_launch_args.py | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pype/hooks/aftereffects/pre_launch_args.py b/pype/hooks/aftereffects/pre_launch_args.py index 01406d495c..e39247b983 100644 --- a/pype/hooks/aftereffects/pre_launch_args.py +++ b/pype/hooks/aftereffects/pre_launch_args.py @@ -9,7 +9,7 @@ class AfterEffectsPrelaunchHook(PreLaunchHook): Hook add python executable and execute python script of AfterEffects implementation before AfterEffects executable. """ - hosts = ["aftereffects"] + app_groups = ["aftereffects"] def execute(self): # Pop tvpaint executable diff --git a/pype/hooks/celaction/pre_celaction_registers.py b/pype/hooks/celaction/pre_celaction_registers.py index f592026699..3f9d81fb98 100644 --- a/pype/hooks/celaction/pre_celaction_registers.py +++ b/pype/hooks/celaction/pre_celaction_registers.py @@ -13,7 +13,7 @@ class CelactionPrelaunchHook(PreLaunchHook): shell script. """ workfile_ext = "scn" - hosts = ["celaction"] + app_groups = ["celaction"] platforms = ["windows"] def execute(self): diff --git a/pype/hooks/fusion/pre_fusion_setup.py b/pype/hooks/fusion/pre_fusion_setup.py index ac7dda4250..d4402e9a04 100644 --- a/pype/hooks/fusion/pre_fusion_setup.py +++ b/pype/hooks/fusion/pre_fusion_setup.py @@ -9,7 +9,7 @@ class FusionPrelaunch(PreLaunchHook): This hook will check if current workfile path has Fusion project inside. """ - hosts = ["fusion"] + app_groups = ["fusion"] def execute(self): # making sure pyton 3.6 is installed at provided path diff --git a/pype/hooks/harmony/pre_launch_args.py b/pype/hooks/harmony/pre_launch_args.py index 70fac5bb76..70c05eb352 100644 --- a/pype/hooks/harmony/pre_launch_args.py +++ b/pype/hooks/harmony/pre_launch_args.py @@ -9,7 +9,7 @@ class HarmonyPrelaunchHook(PreLaunchHook): Hook add python executable and execute python script of harmony implementation before harmony executable. """ - hosts = ["harmony"] + app_groups = ["harmony"] def execute(self): # Pop tvpaint executable diff --git a/pype/hooks/hiero/pre_launch_args.py b/pype/hooks/hiero/pre_launch_args.py index 7139f57bbb..feca6dc3eb 100644 --- a/pype/hooks/hiero/pre_launch_args.py +++ b/pype/hooks/hiero/pre_launch_args.py @@ -4,7 +4,7 @@ from pype.lib import PreLaunchHook class HieroLaunchArguments(PreLaunchHook): order = 0 - hosts = ["hiero"] + app_groups = ["hiero"] def execute(self): """Prepare suprocess launch arguments for Hiero.""" diff --git a/pype/hooks/maya/pre_launch_args.py b/pype/hooks/maya/pre_launch_args.py index 570c8e79b8..8b37bac15b 100644 --- a/pype/hooks/maya/pre_launch_args.py +++ b/pype/hooks/maya/pre_launch_args.py @@ -5,7 +5,7 @@ from pype.lib import PreLaunchHook class MayaLaunchArguments(PreLaunchHook): """Add path to last workfile to launch arguments.""" order = 0 - hosts = ["maya"] + app_groups = ["maya"] def execute(self): """Prepare suprocess launch arguments for Maya.""" diff --git a/pype/hooks/nukestudio/pre_launch_args.py b/pype/hooks/nukestudio/pre_launch_args.py index a5e04bf956..e572ca32a2 100644 --- a/pype/hooks/nukestudio/pre_launch_args.py +++ b/pype/hooks/nukestudio/pre_launch_args.py @@ -4,7 +4,7 @@ from pype.lib import PreLaunchHook class NukeStudioLaunchArguments(PreLaunchHook): order = 0 - hosts = ["nukestudio"] + app_groups = ["nukestudio"] def execute(self): """Prepare suprocess launch arguments for NukeStudio.""" diff --git a/pype/hooks/nukex/pre_launch_args.py b/pype/hooks/nukex/pre_launch_args.py index 39ccb5a58a..f0e5cf7733 100644 --- a/pype/hooks/nukex/pre_launch_args.py +++ b/pype/hooks/nukex/pre_launch_args.py @@ -4,7 +4,7 @@ from pype.lib import PreLaunchHook class NukeXLaunchArguments(PreLaunchHook): order = 0 - hosts = ["nukex"] + app_groups = ["nukex"] def execute(self): """Prepare suprocess launch arguments for NukeX.""" diff --git a/pype/hooks/photoshop/pre_launch_args.py b/pype/hooks/photoshop/pre_launch_args.py index 2c88f62157..b13e7d1e0f 100644 --- a/pype/hooks/photoshop/pre_launch_args.py +++ b/pype/hooks/photoshop/pre_launch_args.py @@ -9,7 +9,7 @@ class PhotoshopPrelaunchHook(PreLaunchHook): Hook add python executable and execute python script of photoshop implementation before photoshop executable. """ - hosts = ["photoshop"] + app_groups = ["photoshop"] def execute(self): # Pop tvpaint executable diff --git a/pype/hooks/resolve/pre_resolve_setup.py b/pype/hooks/resolve/pre_resolve_setup.py index 336fabf0c4..4f6d33c6eb 100644 --- a/pype/hooks/resolve/pre_resolve_setup.py +++ b/pype/hooks/resolve/pre_resolve_setup.py @@ -11,7 +11,7 @@ class ResolvePrelaunch(PreLaunchHook): path to the project by environment variable to Premiere launcher shell script. """ - hosts = ["resolve"] + app_groups = ["resolve"] def execute(self): # making sure pyton 3.6 is installed at provided path diff --git a/pype/hooks/tvpaint/pre_install_pywin.py b/pype/hooks/tvpaint/pre_install_pywin.py index b51267934b..ca9242c4c8 100644 --- a/pype/hooks/tvpaint/pre_install_pywin.py +++ b/pype/hooks/tvpaint/pre_install_pywin.py @@ -9,7 +9,7 @@ class PreInstallPyWin(PreLaunchHook): """Hook makes sure there is installed python module pywin32 on windows.""" # WARNING This hook will probably be deprecated in Pype 3 - kept for test order = 10 - hosts = ["tvpaint"] + app_groups = ["tvpaint"] platforms = ["windows"] def execute(self): diff --git a/pype/hooks/tvpaint/pre_launch_args.py b/pype/hooks/tvpaint/pre_launch_args.py index 9d80e6fddb..13ec320fa0 100644 --- a/pype/hooks/tvpaint/pre_launch_args.py +++ b/pype/hooks/tvpaint/pre_launch_args.py @@ -16,7 +16,7 @@ class TvpaintPrelaunchHook(PreLaunchHook): Existence of last workfile is checked. If workfile does not exists tries to copy templated workfile from predefined path. """ - hosts = ["tvpaint"] + app_groups = ["tvpaint"] def execute(self): # Pop tvpaint executable From 622208c015ed16a3074a3326ebe15c8a963b62ab Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 12:54:23 +0100 Subject: [PATCH 45/54] implemented `ApplicationExecutable` for handlind executablesx and arguments --- pype/lib/applications.py | 68 ++++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index ac45112511..86c640f2ed 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -58,8 +58,8 @@ class ApplictionExecutableNotFound(Exception): " are not available on this machine." ) details = "Defined paths:" - for executable_path in application.executables: - details += "\n- " + executable_path + for executable in application.executables: + details += "\n- " + executable.executable_path self.msg = msg.format(application.full_label, application.app_name) self.details = details @@ -535,7 +535,7 @@ class ApplicationManager: label = variant_definitions.get("label") or app_group variants = variant_definitions.get("variants") or {} icon = variant_definitions.get("icon") - host_name = variant_definitions.get("host_name") or None + group_host_name = variant_definitions.get("host_name") or None for app_name, app_data in variants.items(): if app_name in self.applications: raise AssertionError(( @@ -553,6 +553,8 @@ class ApplicationManager: if not app_data.get("icon"): app_data["icon"] = icon + host_name = app_data.get("host_name") or group_host_name + app_data["is_host"] = host_name is not None self.applications[app_name] = Application( @@ -628,6 +630,41 @@ class ApplicationTool: return self.enabled +class ApplicationExecutable: + def __init__(self, executable): + default_launch_args = [] + if isinstance(executable, str): + executable_path = executable + + elif isinstance(executable, list): + executable_path = None + for arg in executable: + if arg: + if executable_path is None: + executable_path = arg + else: + default_launch_args.append(arg) + + self.executable_path = executable_path + self.default_launch_args = default_launch_args + + def __iter__(self): + yield self.executable_path + for arg in self.default_launch_args: + yield arg + + def __str__(self): + return self.executable_path + + def as_args(self): + return list(self) + + def exists(self): + if not self.executable_path: + return False + return os.path.exists(self.executable_path) + + class Application: """Hold information about application. @@ -658,12 +695,17 @@ class Application: self.enabled = app_data.get("enabled", True) self.is_host = app_data.get("is_host", False) - executables = app_data["executables"] - if isinstance(executables, dict): - executables = executables.get(platform.system().lower()) or [] + _executables = app_data["executables"] + if not _executables: + _executables = [] + + elif isinstance(_executables, dict): + _executables = _executables.get(platform.system().lower()) or [] + + executables = [] + for executable in _executables: + executables.append(ApplicationExecutable(executable)) - if not isinstance(executables, list): - executables = [executables] self.executables = executables @property @@ -683,9 +725,9 @@ class Application: Returns (str): Path to executable from `executables` or None if any exists. """ - for executable_path in self.executables: - if os.path.exists(executable_path): - return executable_path + for executable in self.executables: + if executable.exists(): + return executable return None def launch(self, *args, **kwargs): @@ -845,7 +887,7 @@ class ApplicationLaunchContext: Args: application (Application): Application definition. - executable (str): Path to executable. + executable (ApplicationExecutable): Object with path to executable. **data (dict): Any additional data. Data may be used during preparation to store objects usable in multiple places. """ @@ -868,7 +910,7 @@ class ApplicationLaunchContext: self.data["settings_env"] = settings_env # subprocess.Popen launch arguments (first argument in constructor) - self.launch_args = [executable] + self.launch_args = executable.as_args() # Handle launch environemtns passed_env = self.data.pop("env", None) From 7ab49cdf935229eaee59787e452c20bee3801bf3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 13:00:42 +0100 Subject: [PATCH 46/54] fix check of executable path as it does not have to be full path --- pype/lib/applications.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index 86c640f2ed..fbf949c247 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -1,13 +1,12 @@ import os import sys -import re import getpass -import json import copy import platform import inspect import logging import subprocess +import distutils.spawn from abc import ABCMeta, abstractmethod import six @@ -649,7 +648,7 @@ class ApplicationExecutable: self.default_launch_args = default_launch_args def __iter__(self): - yield self.executable_path + yield distutils.spawn.find_executable(self.executable_path) for arg in self.default_launch_args: yield arg @@ -662,7 +661,7 @@ class ApplicationExecutable: def exists(self): if not self.executable_path: return False - return os.path.exists(self.executable_path) + return bool(distutils.spawn.find_executable(self.executable_path)) class Application: From c16e925cbe43d7ca103981e48a6e319357e042c0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 13:25:07 +0100 Subject: [PATCH 47/54] removed PathInput --- .../settings/settings/widgets/item_types.py | 3 +- .../settings/settings/widgets/widgets.py | 29 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index fd5364ea17..072d0a3ee6 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -4,7 +4,6 @@ from Qt import QtWidgets, QtCore, QtGui from .widgets import ( ExpandingWidget, NumberSpinBox, - PathInput, GridLabelWidget, ComboBox, NiceCheckbox @@ -1094,7 +1093,7 @@ class PathInputWidget(QtWidgets.QWidget, InputObject): layout.addWidget(label_widget, 0) self.label_widget = label_widget - self.input_field = PathInput(self) + self.input_field = QtWidgets.QLineEdit(self) self.setFocusProxy(self.input_field) layout.addWidget(self.input_field, 1) diff --git a/pype/tools/settings/settings/widgets/widgets.py b/pype/tools/settings/settings/widgets/widgets.py index b64b1aa8ac..d25cd5a8e0 100644 --- a/pype/tools/settings/settings/widgets/widgets.py +++ b/pype/tools/settings/settings/widgets/widgets.py @@ -47,35 +47,6 @@ class ComboBox(QtWidgets.QComboBox): return self.itemData(self.currentIndex(), role=QtCore.Qt.UserRole) -class PathInput(QtWidgets.QLineEdit): - def clear_end_path(self): - value = self.text().strip() - if value.endswith("/"): - while value and value[-1] == "/": - value = value[:-1] - self.setText(value) - - def keyPressEvent(self, event): - # Always change backslash `\` for forwardslash `/` - if event.key() == QtCore.Qt.Key_Backslash: - event.accept() - new_event = QtGui.QKeyEvent( - event.type(), - QtCore.Qt.Key_Slash, - event.modifiers(), - "/", - event.isAutoRepeat(), - event.count() - ) - QtWidgets.QApplication.sendEvent(self, new_event) - return - super(PathInput, self).keyPressEvent(event) - - def focusOutEvent(self, event): - super(PathInput, self).focusOutEvent(event) - self.clear_end_path() - - class ClickableWidget(QtWidgets.QWidget): clicked = QtCore.Signal() From a763088d97433f5273c7d5d6d5dd653fcef73f72 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 13:27:00 +0100 Subject: [PATCH 48/54] path input and path widget may be with arguments --- .../settings/settings/widgets/item_types.py | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 072d0a3ee6..167afdf4ce 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1069,7 +1069,7 @@ class TextWidget(QtWidgets.QWidget, InputObject): class PathInputWidget(QtWidgets.QWidget, InputObject): default_input_value = "" value_changed = QtCore.Signal(object) - valid_value_types = (str, ) + valid_value_types = (str, list) def __init__( self, input_data, parent, @@ -1081,6 +1081,8 @@ class PathInputWidget(QtWidgets.QWidget, InputObject): self.initial_attributes(input_data, parent, as_widget) + self.with_arguments = input_data.get("with_arguments", False) + layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(5) @@ -1094,21 +1096,36 @@ class PathInputWidget(QtWidgets.QWidget, InputObject): self.label_widget = label_widget self.input_field = QtWidgets.QLineEdit(self) + self.args_input_field = None + if self.with_arguments: + self.input_field.setPlaceholderText("Executable path") + self.args_input_field = QtWidgets.QLineEdit(self) + self.args_input_field.setPlaceholderText("Arguments") + self.setFocusProxy(self.input_field) layout.addWidget(self.input_field, 1) - self.input_field.textChanged.connect(self._on_value_change) + if self.args_input_field: + layout.addWidget(self.args_input_field, 1) + self.args_input_field.textChanged.connect(self._on_value_change) + def set_value(self, value): self.validate_value(value) - self.input_field.setText(value) - def focusOutEvent(self, event): - self.input_field.clear_end_path() - super(PathInput, self).focusOutEvent(event) + if not isinstance(value, list): + self.input_field.setText(value) + elif self.with_arguments: + self.input_field.setText(value[0]) + self.args_input_field.setText(value[1]) + else: + self.input_field.setText(value[0]) def item_value(self): - return self.input_field.text() + path_value = self.input_field.text() + if self.with_arguments: + return [path_value, self.args_input_field.text()] + return path_value class EnumeratorWidget(QtWidgets.QWidget, InputObject): @@ -3154,6 +3171,7 @@ class PathWidget(QtWidgets.QWidget, SettingObject): self.multiplatform = input_data.get("multiplatform", False) self.multipath = input_data.get("multipath", False) + self.with_arguments = input_data.get("with_arguments", False) self.input_field = None @@ -3195,7 +3213,10 @@ class PathWidget(QtWidgets.QWidget, SettingObject): def create_gui(self): if not self.multiplatform and not self.multipath: - input_data = {"key": self.key} + input_data = { + "key": self.key, + "with_arguments": self.with_arguments + } path_input = PathInputWidget( input_data, self, as_widget=True, label_widget=self.label_widget @@ -3209,7 +3230,10 @@ class PathWidget(QtWidgets.QWidget, SettingObject): if not self.multiplatform: item_schema = { "key": self.key, - "object_type": "path-input" + "object_type": { + "type": "path-input", + "with_arguments": self.with_arguments + } } input_widget = ListWidget( item_schema, self, @@ -3234,9 +3258,13 @@ class PathWidget(QtWidgets.QWidget, SettingObject): } if self.multipath: child_item["type"] = "list" - child_item["object_type"] = "path-input" + child_item["object_type"] = { + "type": "path-input", + "with_arguments": self.with_arguments + } else: child_item["type"] = "path-input" + child_item["with_arguments"] = self.with_arguments item_schema["children"].append(child_item) From c089184789dd118414e42c1f577e01965d014675 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 13:30:51 +0100 Subject: [PATCH 49/54] hosts executables have set `with_arguments` to true --- .../system_settings/global/applications.json | 185 ++++++++++++++---- .../host_settings/template_host_variant.json | 3 +- 2 files changed, 150 insertions(+), 38 deletions(-) diff --git a/pype/settings/defaults/system_settings/global/applications.json b/pype/settings/defaults/system_settings/global/applications.json index 7dd0057110..cce4c4f25e 100644 --- a/pype/settings/defaults/system_settings/global/applications.json +++ b/pype/settings/defaults/system_settings/global/applications.json @@ -39,11 +39,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe" + [ + "C:\\Program Files\\Autodesk\\Maya2020\\bin\\maya.exe", + "" + ] ], "darwin": [], "linux": [ - "/usr/autodesk/maya2020/bin/maya" + [ + "/usr/autodesk/maya2020/bin/maya", + "" + ] ] }, "environment": { @@ -62,11 +68,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe" + [ + "C:\\Program Files\\Autodesk\\Maya2019\\bin\\maya.exe", + "" + ] ], "darwin": [], "linux": [ - "/usr/autodesk/maya2019/bin/maya" + [ + "/usr/autodesk/maya2019/bin/maya", + "" + ] ] }, "environment": { @@ -85,11 +97,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Autodesk\\Maya2017\\bin\\maya.exe" + [ + "C:\\Program Files\\Autodesk\\Maya2017\\bin\\maya.exe", + "" + ] ], "darwin": [], "linux": [ - "/usr/autodesk/maya2018/bin/maya" + [ + "/usr/autodesk/maya2018/bin/maya", + "" + ] ] }, "environment": { @@ -143,7 +161,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Autodesk\\Maya2020\\bin\\mayabatch.exe" + [ + "C:\\Program Files\\Autodesk\\Maya2020\\bin\\mayabatch.exe", + "" + ] ], "darwin": [], "linux": [] @@ -164,7 +185,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Autodesk\\Maya2019\\bin\\mayabatch.exe" + [ + "C:\\Program Files\\Autodesk\\Maya2019\\bin\\mayabatch.exe", + "" + ] ], "darwin": [], "linux": [] @@ -185,7 +209,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Autodesk\\Maya2018\\bin\\mayabatch.exe" + [ + "C:\\Program Files\\Autodesk\\Maya2018\\bin\\mayabatch.exe", + "" + ] ], "darwin": [], "linux": [] @@ -230,11 +257,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe" + [ + "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe", + "" + ] ], "darwin": [], "linux": [ - "/usr/local/Nuke12.0v1/Nuke12.0" + [ + "/usr/local/Nuke12.0v1/Nuke12.0", + "" + ] ] }, "environment": { @@ -250,11 +283,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe" + [ + "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe", + "" + ] ], "darwin": [], "linux": [ - "/usr/local/Nuke11.3v5/Nuke11.3" + [ + "/usr/local/Nuke11.3v5/Nuke11.3", + "" + ] ] }, "environment": { @@ -270,7 +309,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe" + [ + "C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe", + "" + ] ], "darwin": [], "linux": [] @@ -312,11 +354,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe" + [ + "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe", + "--nukex" + ] ], "darwin": [], "linux": [ - "/usr/local/Nuke12.0v1/Nuke12.0" + [ + "/usr/local/Nuke12.0v1/Nuke12.0", + "--nukex" + ] ] }, "environment": { @@ -332,11 +380,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe" + [ + "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe", + "--nukex" + ] ], "darwin": [], "linux": [ - "/usr/local/Nuke11.3v5/Nuke11.3" + [ + "/usr/local/Nuke11.3v5/Nuke11.3", + "--nukex" + ] ] }, "environment": { @@ -352,7 +406,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe" + [ + "C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe", + "--nukex" + ] ], "darwin": [], "linux": [] @@ -398,11 +455,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe" + [ + "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe", + "--studio" + ] ], "darwin": [], "linux": [ - "/usr/local/Nuke12.0v1/Nuke12.0" + [ + "/usr/local/Nuke12.0v1/Nuke12.0", + "--studio" + ] ] }, "environment": { @@ -418,11 +481,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe" + [ + "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe", + "--studio" + ] ], "darwin": [], "linux": [ - "/usr/local/Nuke11.3v5/Nuke11.3" + [ + "/usr/local/Nuke11.3v5/Nuke11.3", + "--studio" + ] ] }, "environment": { @@ -482,11 +551,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe" + [ + "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe", + "--hiero" + ] ], "darwin": [], "linux": [ - "/usr/local/Nuke12.0v1/Nuke12.0" + [ + "/usr/local/Nuke12.0v1/Nuke12.0", + "--hiero" + ] ] }, "environment": { @@ -502,11 +577,17 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe" + [ + "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe", + "--hiero" + ] ], "darwin": [], "linux": [ - "/usr/local/Nuke11.3v5/Nuke11.3" + [ + "/usr/local/Nuke11.3v5/Nuke11.3", + "--hiero" + ] ] }, "environment": { @@ -522,7 +603,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe" + [ + "C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe", + "--hiero" + ] ], "darwin": [], "linux": [] @@ -843,7 +927,10 @@ "executables": { "windows": [], "darwin": [ - "/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium" + [ + "/Applications/Toon Boom Harmony 17 Premium/Harmony Premium.app/Contents/MacOS/Harmony Premium", + "" + ] ], "linux": [] }, @@ -876,7 +963,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe" + [ + "C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe", + "" + ] ], "darwin": [], "linux": [] @@ -894,7 +984,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe" + [ + "C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe", + "" + ] ], "darwin": [], "linux": [] @@ -936,7 +1029,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe" + [ + "C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe", + "" + ] ], "darwin": [], "linux": [] @@ -954,7 +1050,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe" + [ + "C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe", + "" + ] ], "darwin": [], "linux": [] @@ -996,7 +1095,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Adobe\\Adobe After Effects 2020\\Support Files\\AfterFX.exe" + [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2020\\Support Files\\AfterFX.exe", + "" + ] ], "darwin": [], "linux": [] @@ -1014,7 +1116,10 @@ "icon": "", "executables": { "windows": [ - "C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe" + [ + "C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe", + "" + ] ], "darwin": [], "linux": [] @@ -1046,7 +1151,10 @@ "label": "", "variant_label": "Local", "icon": "{}/app_icons/celaction_local.png", - "executables": "", + "executables": [ + "", + "" + ], "environment": { "__environment_keys__": { "celation_Local": [] @@ -1058,7 +1166,10 @@ "label": "", "variant_label": "Pulblish", "icon": "", - "executables": "", + "executables": [ + "", + "" + ], "environment": { "__environment_keys__": { "celation_Publish": [] diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/template_host_variant.json b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/template_host_variant.json index ce3a75e871..cea7da3a81 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/template_host_variant.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/template_host_variant.json @@ -43,7 +43,8 @@ "key": "executables", "label": "Executables", "multiplatform": "{multiplatform}", - "multipath": "{multipath_executables}" + "multipath": "{multipath_executables}", + "with_arguments": true }, { "key": "environment", From 4b3374ac8b14e55503f47fdd5580ba1ff665846c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 13:38:51 +0100 Subject: [PATCH 50/54] avoid accidental combobox value changes on wheel event --- pype/tools/settings/settings/widgets/widgets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pype/tools/settings/settings/widgets/widgets.py b/pype/tools/settings/settings/widgets/widgets.py index d25cd5a8e0..dc2ccabe10 100644 --- a/pype/tools/settings/settings/widgets/widgets.py +++ b/pype/tools/settings/settings/widgets/widgets.py @@ -32,6 +32,11 @@ class ComboBox(QtWidgets.QComboBox): super(ComboBox, self).__init__(*args, **kwargs) self.currentIndexChanged.connect(self._on_change) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + def wheelEvent(self, event): + if self.hasFocus(): + return super(ComboBox, self).wheelEvent(event) def _on_change(self, *args, **kwargs): self.value_changed.emit() From 103e17f7b309b8012ad8bcb285e5418d32ed795d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 30 Nov 2020 17:51:18 +0100 Subject: [PATCH 51/54] fix settings loading --- pype/lib/applications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/lib/applications.py b/pype/lib/applications.py index fbf949c247..ddff9ddcd2 100644 --- a/pype/lib/applications.py +++ b/pype/lib/applications.py @@ -528,7 +528,7 @@ class ApplicationManager: """Refresh applications from settings.""" settings = system_settings() - hosts_definitions = settings["global"]["applications"] + hosts_definitions = settings["applications"] for app_group, variant_definitions in hosts_definitions.items(): enabled = variant_definitions["enabled"] label = variant_definitions.get("label") or app_group @@ -560,7 +560,7 @@ class ApplicationManager: app_group, app_name, host_name, app_data, self ) - tools_definitions = settings["global"]["tools"] + tools_definitions = settings["tools"] for tool_group_name, tool_group_data in tools_definitions.items(): enabled = tool_group_data.get("enabled", True) tool_variants = tool_group_data.get("variants") or {} From 0073903eceef8dd6617c5c24172e8db5cbfd3d0a Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 1 Dec 2020 16:38:53 +0100 Subject: [PATCH 52/54] change executable and arguments visual ratio --- pype/tools/settings/settings/widgets/item_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 1fe8003716..8a1788188d 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1103,11 +1103,11 @@ class PathInputWidget(QtWidgets.QWidget, InputObject): self.args_input_field.setPlaceholderText("Arguments") self.setFocusProxy(self.input_field) - layout.addWidget(self.input_field, 1) + layout.addWidget(self.input_field, 8) self.input_field.textChanged.connect(self._on_value_change) if self.args_input_field: - layout.addWidget(self.args_input_field, 1) + layout.addWidget(self.args_input_field, 2) self.args_input_field.textChanged.connect(self._on_value_change) def set_value(self, value): From 2afe4484a63d6299ab38a595e758be1d45a92249 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 1 Dec 2020 18:15:18 +0100 Subject: [PATCH 53/54] fixed conflict changes --- pype/tools/settings/settings/widgets/item_types.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pype/tools/settings/settings/widgets/item_types.py b/pype/tools/settings/settings/widgets/item_types.py index 7f78d672dd..1f5615a240 100644 --- a/pype/tools/settings/settings/widgets/item_types.py +++ b/pype/tools/settings/settings/widgets/item_types.py @@ -1094,6 +1094,8 @@ class PathInputWidget(QtWidgets.QWidget, InputObject): self.initial_attributes(schema_data, parent, as_widget) + self.with_arguments = schema_data.get("with_arguments", False) + def create_ui(self, label_widget=None): layout = QtWidgets.QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -3205,6 +3207,7 @@ class PathWidget(QtWidgets.QWidget, SettingObject): self.multiplatform = schema_data.get("multiplatform", False) self.multipath = schema_data.get("multipath", False) + self.with_arguments = schema_data.get("with_arguments", False) self.input_field = None @@ -3244,8 +3247,11 @@ class PathWidget(QtWidgets.QWidget, SettingObject): def create_ui_inputs(self): if not self.multiplatform and not self.multipath: - input_data = {"key": self.key} - path_input = PathInputWidget(input_data, self, as_widget=True) + item_schema = { + "key": self.key, + "with_arguments": self.with_arguments + } + path_input = PathInputWidget(item_schema, self, as_widget=True) path_input.create_ui(label_widget=self.label_widget) self.setFocusProxy(path_input) From cabf5584958874d350a91411d2437d16df9c4466 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 1 Dec 2020 23:08:41 +0100 Subject: [PATCH 54/54] tweaks to harmony and blender launchers --- .../defaults/system_settings/applications.json | 14 ++++++++++++-- .../host_settings/schema_harmony.json | 8 -------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pype/settings/defaults/system_settings/applications.json b/pype/settings/defaults/system_settings/applications.json index cce4c4f25e..e5cd249ffe 100644 --- a/pype/settings/defaults/system_settings/applications.json +++ b/pype/settings/defaults/system_settings/applications.json @@ -827,7 +827,12 @@ "variant_label": "2.90", "icon": "", "executables": { - "windows": [], + "windows": [ + [ + "C:\\Program Files\\Blender Foundation\\Blender 2.90\\blender.exe", + "" + ] + ], "darwin": [], "linux": [] }, @@ -843,7 +848,12 @@ "variant_label": "2.83", "icon": "", "executables": { - "windows": [], + "windows": [ + [ + "C:\\Program Files\\Blender Foundation\\Blender 2.83\\blender.exe", + "" + ] + ], "darwin": [], "linux": [] }, diff --git a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_harmony.json b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_harmony.json index d7b9e61bda..8ca793f90b 100644 --- a/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_harmony.json +++ b/pype/tools/settings/settings/gui_schemas/system_schema/host_settings/schema_harmony.json @@ -30,14 +30,6 @@ "host_version": "20", "host_name": "harmony" }, - { - "host_version": "19", - "host_name": "harmony" - }, - { - "host_version": "18", - "host_name": "harmony" - }, { "host_version": "17", "host_name": "harmony"