From d9bde0c3c47012ae3a8ddf090822089344109708 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 18:38:43 +0200 Subject: [PATCH 01/22] implemented base classes for host implementation --- openpype/host/__init__.py | 13 ++ openpype/host/host.py | 308 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 openpype/host/__init__.py create mode 100644 openpype/host/host.py diff --git a/openpype/host/__init__.py b/openpype/host/__init__.py new file mode 100644 index 0000000000..fcc9372a99 --- /dev/null +++ b/openpype/host/__init__.py @@ -0,0 +1,13 @@ +from .host import ( + HostImplementation, + IWorkfileHost, + ILoadHost, + INewPublisher, +) + +__all__ = ( + "HostImplementation", + "IWorkfileHost", + "ILoadHost", + "INewPublisher", +) diff --git a/openpype/host/host.py b/openpype/host/host.py new file mode 100644 index 0000000000..d311f752cd --- /dev/null +++ b/openpype/host/host.py @@ -0,0 +1,308 @@ +from abc import ABCMeta, abstractproperty, abstractmethod +import six + + +@six.add_metaclass(ABCMeta) +class HostImplementation(object): + """Base of host implementation class. + + Host is pipeline implementation of DCC application. This class should help + to identify what must/should/can be implemented for specific functionality. + + Compared to 'avalon' concept: + What was before considered as functions in host implementation folder. The + host implementation should primarily care about adding ability of creation + (mark subsets to be published) and optionaly about referencing published + representations as containers. + + Host may need extend some functionality like working with workfiles + or loading. Not all host implementations may allow that for those purposes + can be logic extended with implementing functions for the purpose. There + are prepared interfaces to be able identify what must be implemented to + be able use that functionality. + - current statement is that it is not required to inherit from interfaces + but all of the methods are validated (only their existence!) + + # Installation of host before (avalon concept): + ```python + from openpype.pipeline import install_host + import openpype.hosts.maya.api as host + + install_host(host) + ``` + + # Installation of host now: + ```python + from openpype.pipeline import install_host + from openpype.hosts.maya.api import MayaHost + + host = MayaHost() + install_host(host) + ``` + + # TODOs + - move content of 'install_host' as method of this class + - register host object + - install legacy_io + - install global plugin paths + - store registered plugin paths to this object + - handle current context (project, asset, task) + - this must be done in many separated steps + - have it's object of host tools instead of using globals + + This implementation will probably change over time when more + functionality and responsibility will be added. + """ + + def __init__(self): + """Initialization of host. + + Register DCC callbacks, host specific plugin paths, targets etc. + (Part of what 'install' did in 'avalon' concept.) + + NOTE: + At this moment global "installation" must happen before host + installation. Because of this current limitation it is recommended + to implement 'install' method which is triggered after global + 'install'. + """ + + pass + + @abstractproperty + def name(self): + """Host implementation name.""" + + pass + + def get_current_context(self): + """Get current context information. + + This method should be used to get current context of host. Usage of + this method can be crutial for host implementations in DCCs where + can be opened multiple workfiles at one moment and change of context + can't be catched properly. + + Default implementation returns values from 'legacy_io.Session'. + + Returns: + dict: Context with 3 keys 'project_name', 'asset_name' and + 'task_name'. All of them can be 'None'. + """ + + from openpype.pipeline import legacy_io + + if legacy_io.is_installed(): + legacy_io.install() + + return { + "project_name": legacy_io.Session["AVALON_PROJECT"], + "asset_name": legacy_io.Session["AVALON_ASSET"], + "task_name": legacy_io.Session["AVALON_TASK"] + } + + def get_context_title(self): + """Context title shown for UI purposes. + + Should return current context title if possible. + + NOTE: This method is used only for UI purposes so it is possible to + return some logical title for contextless cases. + + Is not meant for "Context menu" label. + + Returns: + str: Context title. + None: Default title is used based on UI implementation. + """ + + # Use current context to fill the context title + current_context = self.get_current_context() + project_name = current_context["project_name"] + asset_name = current_context["asset_name"] + task_name = current_context["task_name"] + items = [] + if project_name: + items.append(project_name) + if asset_name: + items.append(asset_name) + if task_name: + items.append(task_name) + if items: + return "/".join(items) + return None + + +class ILoadHost: + """Implementation requirements to be able use reference of representations. + + The load plugins can do referencing even without implementation of methods + here, but switch and removement of containers would not be possible. + + QUESTIONS + - Is list container dependency of host or load plugins? + - Should this be directly in HostImplementation? + - how to find out if referencing is available? + - do we need to know that? + """ + + @abstractmethod + def ls(self): + """Retreive referenced containers from scene. + + This can be implemented in hosts where referencing can be used. + + TODO: + Rename function to something more self explanatory. + Suggestion: 'get_referenced_containers' + + Returns: + list[dict]: Information about loaded containers. + """ + return [] + + +@six.add_metaclass(ABCMeta) +class IWorkfileHost: + """Implementation requirements to be able use workfile utils and tool. + + This interface just provides what is needed to implement in host + implementation to support workfiles workflow, but does not have necessarily + to inherit from this interface. + """ + + @abstractmethod + def file_extensions(self): + """Extensions that can be used as save. + + QUESTION: This could potentially use 'HostDefinition'. + + TODO: + Rename to 'get_workfile_extensions'. + """ + + return [] + + @abstractmethod + def save_file(self, dst_path=None): + """Save currently opened scene. + + TODO: + Rename to 'save_current_workfile'. + + Args: + dst_path (str): Where the current scene should be saved. Or use + current path if 'None' is passed. + """ + + pass + + @abstractmethod + def open_file(self, filepath): + """Open passed filepath in the host. + + TODO: + Rename to 'open_workfile'. + + Args: + filepath (str): Path to workfile. + """ + + pass + + @abstractmethod + def current_file(self): + """Retreive path to current opened file. + + TODO: + Rename to 'get_current_workfile'. + + Returns: + str: Path to file which is currently opened. + None: If nothing is opened. + """ + + return None + + def has_unsaved_changes(self): + """Currently opened scene is saved. + + Not all hosts can know if current scene is saved because the API of + DCC does not support it. + + Returns: + bool: True if scene is saved and False if has unsaved + modifications. + None: Can't tell if workfiles has modifications. + """ + + return None + + def work_root(self, session): + """Modify workdir per host. + + WARNING: + We must handle this modification with more sofisticated way because + this can't be called out of DCC so opening of last workfile + (calculated before DCC is launched) is complicated. Also breaking + defined work template is not a good idea. + Only place where it's really used and can make sense is Maya. There + workspace.mel can modify subfolders where to look for maya files. + + Default implementation keeps workdir untouched. + + Args: + session (dict): Session context data. + + Returns: + str: Path to new workdir. + """ + + return session["AVALON_WORKDIR"] + + +class INewPublisher: + """Functions related to new creation system in new publisher. + + New publisher is not storing information only about each created instance + but also some global data. At this moment are data related only to context + publish plugins but that can extend in future. + + HostImplementation does not have to inherit from this interface just have + to imlement mentioned all listed methods. + """ + + @abstractmethod + def get_context_data(self): + """Get global data related to creation-publishing from workfile. + + These data are not related to any created instance but to whole + publishing context. Not saving/returning them will cause that each + reset of publishing resets all values to default ones. + + Context data can contain information about enabled/disabled publish + plugins or other values that can be filled by artist. + + Returns: + dict: Context data stored using 'update_context_data'. + """ + + pass + + @abstractmethod + def update_context_data(self, data, changes): + """Store global context data to workfile. + + Called when some values in context data has changed. + + Without storing the values in a way that 'get_context_data' would + return them will each reset of publishing cause loose of filled values + by artist. Best practice is to store values into workfile, if possible. + + Args: + data (dict): New data as are. + changes (dict): Only data that has been changed. Each value has + tuple with '(, )' value. + """ + + pass From 626e6f9b7d734b072d99b0820c18e58a2880a8fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 18:39:41 +0200 Subject: [PATCH 02/22] moved import of settings to top --- openpype/hosts/maya/api/pipeline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index d9276ddf4a..5905251e93 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -8,6 +8,7 @@ import maya.api.OpenMaya as om import pyblish.api +from openpype.settings import get_project_settings import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( @@ -46,8 +47,6 @@ self._events = {} def install(): - from openpype.settings import get_project_settings - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) # process path mapping dirmap_processor = MayaDirmap("maya", project_settings) From 7f106cad33602b125be17220f2124050c546350e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 18:51:30 +0200 Subject: [PATCH 03/22] added maintained_selection to HostImplementation --- openpype/host/host.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/host/host.py b/openpype/host/host.py index d311f752cd..4851ee59bf 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -1,3 +1,4 @@ +import contextlib from abc import ABCMeta, abstractproperty, abstractmethod import six @@ -132,6 +133,19 @@ class HostImplementation(object): return "/".join(items) return None + @contextlib.contextmanager + def maintained_selection(self): + """Some functionlity will happen but selection should stay same. + + This is DCC specific. Some may not allow to implement this ability + that is reason why default implementation is empty context manager. + """ + + try: + yield + finally: + pass + class ILoadHost: """Implementation requirements to be able use reference of representations. From 7ac1b6cadc60fab4d32207f6e5fd640f1d492cc8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 19:18:01 +0200 Subject: [PATCH 04/22] added logger to HostImplementation --- openpype/host/host.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/host/host.py b/openpype/host/host.py index 4851ee59bf..27b22e4850 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -1,3 +1,4 @@ +import logging import contextlib from abc import ABCMeta, abstractproperty, abstractmethod import six @@ -55,6 +56,8 @@ class HostImplementation(object): functionality and responsibility will be added. """ + _log = None + def __init__(self): """Initialization of host. @@ -70,6 +73,12 @@ class HostImplementation(object): pass + @property + def log(self): + if self._log is None: + self._log = logging.getLogger(self.__class__.__name__) + return self._log + @abstractproperty def name(self): """Host implementation name.""" From 0ff8e2bcc686f1dbae795450d9635e0be7c3475d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 19:30:39 +0200 Subject: [PATCH 05/22] added validation methods to interfaces --- openpype/host/host.py | 112 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/openpype/host/host.py b/openpype/host/host.py index 27b22e4850..302c181598 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -4,6 +4,17 @@ from abc import ABCMeta, abstractproperty, abstractmethod import six +class MissingMethodsError(ValueError): + def __init__(self, host, missing_methods): + joined_missing = ", ".join( + ['"{}"'.format(item) for item in missing_methods] + ) + message = ( + "Host \"{}\" miss methods {}".format(host.name, joined_missing) + ) + super(MissingMethodsError, self).__init__(message) + + @six.add_metaclass(ABCMeta) class HostImplementation(object): """Base of host implementation class. @@ -169,6 +180,36 @@ class ILoadHost: - do we need to know that? """ + @staticmethod + def get_missing_load_methods(host): + """Look for missing methods on host implementation. + + Method is used for validation of implemented functions related to + loading. Checks only existence of methods. + + Args: + list[str]: Missing method implementations for loading workflow. + """ + + required = ["ls"] + missing = [] + for name in required: + if not hasattr(host, name): + missing.append(name) + return missing + + @staticmethod + def validate_load_methods(host): + """Validate implemented methods of host for load workflow. + + Raises: + MissingMethodsError: If there are missing methods on host + implementation. + """ + missing = ILoadHost.get_missing_load_methods(host) + if missing: + raise MissingMethodsError(host, missing) + @abstractmethod def ls(self): """Retreive referenced containers from scene. @@ -194,6 +235,43 @@ class IWorkfileHost: to inherit from this interface. """ + @staticmethod + def get_missing_workfile_methods(host): + """Look for missing methods on host implementation. + + Method is used for validation of implemented functions related to + workfiles. Checks only existence of methods. + + Returns: + list[str]: Missing method implementations for workfiles workflow. + """ + + required = [ + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root", + ] + missing = [] + for name in required: + if not hasattr(host, name): + missing.append(name) + return missing + + @staticmethod + def validate_workfile_methods(host): + """Validate implemented methods of host for workfiles workflow. + + Raises: + MissingMethodsError: If there are missing methods on host + implementation. + """ + missing = IWorkfileHost.get_missing_workfile_methods(host) + if missing: + raise MissingMethodsError(host, missing) + @abstractmethod def file_extensions(self): """Extensions that can be used as save. @@ -295,6 +373,40 @@ class INewPublisher: to imlement mentioned all listed methods. """ + @staticmethod + def get_missing_publish_methods(host): + """Look for missing methods on host implementation. + + Method is used for validation of implemented functions related to + new publish creation. Checks only existence of methods. + + Args: + list[str]: Missing method implementations for new publsher + workflow. + """ + + required = [ + "get_context_data", + "update_context_data", + ] + missing = [] + for name in required: + if not hasattr(host, name): + missing.append(name) + return missing + + @staticmethod + def validate_publish_methods(host): + """Validate implemented methods of host for create-publish workflow. + + Raises: + MissingMethodsError: If there are missing methods on host + implementation. + """ + missing = INewPublisher.get_missing_publish_methods(host) + if missing: + raise MissingMethodsError(host, missing) + @abstractmethod def get_context_data(self): """Get global data related to creation-publishing from workfile. From b81dbf9ee411a2a02d46da9f144c78eca4ddcf21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 19:31:37 +0200 Subject: [PATCH 06/22] use validations from interfaces --- openpype/pipeline/create/context.py | 15 +++++---------- openpype/tools/utils/host_tools.py | 15 ++++++++++----- openpype/tools/workfiles/__init__.py | 2 -- openpype/tools/workfiles/app.py | 28 ++-------------------------- 4 files changed, 17 insertions(+), 43 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 2f1922c103..4bfa68c9d4 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -6,6 +6,7 @@ import inspect from uuid import uuid4 from contextlib import contextmanager +from openpype.host import INewPublisher from openpype.pipeline import legacy_io from openpype.pipeline.mongodb import ( AvalonMongoDB, @@ -651,12 +652,6 @@ class CreateContext: discover_publish_plugins(bool): Discover publish plugins during reset phase. """ - # Methods required in host implementaion to be able create instances - # or change context data. - required_methods = ( - "get_context_data", - "update_context_data" - ) def __init__( self, host, dbcon=None, headless=False, reset=True, @@ -738,10 +733,10 @@ class CreateContext: Args: host(ModuleType): Host implementaion. """ - missing = set() - for attr_name in cls.required_methods: - if not hasattr(host, attr_name): - missing.add(attr_name) + + missing = set( + INewPublisher.get_missing_publish_methods(host) + ) return missing @property diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 9dbbe25fda..ae23e4d089 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -6,7 +6,7 @@ use singleton approach with global functions (using helper anyway). import os import pyblish.api - +from openpype.host import IWorkfileHost, ILoadHost from openpype.pipeline import ( registered_host, legacy_io, @@ -49,12 +49,11 @@ class HostToolsHelper: def get_workfiles_tool(self, parent): """Create, cache and return workfiles tool window.""" if self._workfiles_tool is None: - from openpype.tools.workfiles.app import ( - Window, validate_host_requirements - ) + from openpype.tools.workfiles.app import Window + # Host validation host = registered_host() - validate_host_requirements(host) + IWorkfileHost.validate_workfile_methods(host) workfiles_window = Window(parent=parent) self._workfiles_tool = workfiles_window @@ -92,6 +91,9 @@ class HostToolsHelper: if self._loader_tool is None: from openpype.tools.loader import LoaderWindow + host = registered_host() + ILoadHost.validate_load_methods(host) + loader_window = LoaderWindow(parent=parent or self._parent) self._loader_tool = loader_window @@ -164,6 +166,9 @@ class HostToolsHelper: if self._scene_inventory_tool is None: from openpype.tools.sceneinventory import SceneInventoryWindow + host = registered_host() + ILoadHost.validate_load_methods(host) + scene_inventory_window = SceneInventoryWindow( parent=parent or self._parent ) diff --git a/openpype/tools/workfiles/__init__.py b/openpype/tools/workfiles/__init__.py index 5fbc71797d..205fd44838 100644 --- a/openpype/tools/workfiles/__init__.py +++ b/openpype/tools/workfiles/__init__.py @@ -1,12 +1,10 @@ from .window import Window from .app import ( show, - validate_host_requirements, ) __all__ = [ "Window", "show", - "validate_host_requirements", ] diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 352847ede8..2d0d551faf 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -1,6 +1,7 @@ import sys import logging +from openpype.host import IWorkfileHost from openpype.pipeline import ( registered_host, legacy_io, @@ -14,31 +15,6 @@ module = sys.modules[__name__] module.window = None -def validate_host_requirements(host): - if host is None: - raise RuntimeError("No registered host.") - - # Verify the host has implemented the api for Work Files - required = [ - "open_file", - "save_file", - "current_file", - "has_unsaved_changes", - "work_root", - "file_extensions", - ] - missing = [] - for name in required: - if not hasattr(host, name): - missing.append(name) - if missing: - raise RuntimeError( - "Host is missing required Work Files interfaces: " - "%s (host: %s)" % (", ".join(missing), host) - ) - return True - - def show(root=None, debug=False, parent=None, use_context=True, save=True): """Show Work Files GUI""" # todo: remove `root` argument to show() @@ -50,7 +26,7 @@ def show(root=None, debug=False, parent=None, use_context=True, save=True): pass host = registered_host() - validate_host_requirements(host) + IWorkfileHost.validate_workfile_methods(host) if debug: legacy_io.Session["AVALON_ASSET"] = "Mock" From 3aa88814a1f6efbfb0d92ed6119c44e122b2fef4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 19:31:52 +0200 Subject: [PATCH 07/22] added is_installed function to legacy_io --- openpype/pipeline/legacy_io.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/legacy_io.py b/openpype/pipeline/legacy_io.py index c8e7e79600..d586756671 100644 --- a/openpype/pipeline/legacy_io.py +++ b/openpype/pipeline/legacy_io.py @@ -18,9 +18,13 @@ _database = database = None log = logging.getLogger(__name__) +def is_installed(): + return module._is_installed + + def install(): """Establish a persistent connection to the database""" - if module._is_installed: + if is_installed(): return session = session_data_from_environment(context_keys=True) @@ -55,7 +59,7 @@ def uninstall(): def requires_install(func): @functools.wraps(func) def decorated(*args, **kwargs): - if not module._is_installed: + if not is_installed(): install() return func(*args, **kwargs) return decorated From 97e6aa4120b24df7738c1635bcfeb14f1136663d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 19:32:31 +0200 Subject: [PATCH 08/22] use HostImplementation class in Maya host --- openpype/hosts/maya/api/__init__.py | 4 +- openpype/hosts/maya/api/pipeline.py | 186 ++++++++++++++--------- openpype/hosts/maya/startup/userSetup.py | 5 +- 3 files changed, 120 insertions(+), 75 deletions(-) diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 5d76bf0f04..6c28c59580 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -5,11 +5,11 @@ Anything that isn't defined here is INTERNAL and unreliable for external use. """ from .pipeline import ( - install, uninstall, ls, containerise, + MayaHostImplementation, ) from .plugin import ( Creator, @@ -40,11 +40,11 @@ from .lib import ( __all__ = [ - "install", "uninstall", "ls", "containerise", + "MayaHostImplementation", "Creator", "Loader", diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 5905251e93..4924185f0a 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -1,7 +1,7 @@ import os -import sys import errno import logging +import contextlib from maya import utils, cmds, OpenMaya import maya.api.OpenMaya as om @@ -9,6 +9,7 @@ import maya.api.OpenMaya as om import pyblish.api from openpype.settings import get_project_settings +from openpype.host import HostImplementation import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( @@ -29,6 +30,14 @@ from openpype.pipeline import ( ) from openpype.hosts.maya.lib import copy_workspace_mel from . import menu, lib +from .workio import ( + open_file, + save_file, + file_extensions, + has_unsaved_changes, + work_root, + current_file +) log = logging.getLogger("openpype.hosts.maya") @@ -41,47 +50,121 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" -self = sys.modules[__name__] -self._ignore_lock = False -self._events = {} +class MayaHostImplementation(HostImplementation): + name = "maya" -def install(): - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) - # process path mapping - dirmap_processor = MayaDirmap("maya", project_settings) - dirmap_processor.process_dirmap() + def __init__(self): + super(MayaHostImplementation, self).__init__() + self._events = {} - pyblish.api.register_plugin_path(PUBLISH_PATH) - pyblish.api.register_host("mayabatch") - pyblish.api.register_host("mayapy") - pyblish.api.register_host("maya") + def install(self): + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + # process path mapping + dirmap_processor = MayaDirmap("maya", project_settings) + dirmap_processor.process_dirmap() - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) - register_inventory_action_path(INVENTORY_PATH) - log.info(PUBLISH_PATH) + pyblish.api.register_plugin_path(PUBLISH_PATH) + pyblish.api.register_host("mayabatch") + pyblish.api.register_host("mayapy") + pyblish.api.register_host("maya") - log.info("Installing callbacks ... ") - register_event_callback("init", on_init) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + register_inventory_action_path(INVENTORY_PATH) + self.log.info(PUBLISH_PATH) - if lib.IS_HEADLESS: - log.info(("Running in headless mode, skipping Maya " - "save/open/new callback installation..")) + self.log.info("Installing callbacks ... ") + register_event_callback("init", on_init) - return + if lib.IS_HEADLESS: + self.log.info(( + "Running in headless mode, skipping Maya save/open/new" + " callback installation.." + )) - _set_project() - _register_callbacks() + return - menu.install() + _set_project() + self._register_callbacks() - register_event_callback("save", on_save) - register_event_callback("open", on_open) - register_event_callback("new", on_new) - register_event_callback("before.save", on_before_save) - register_event_callback("taskChanged", on_task_changed) - register_event_callback("workfile.save.before", before_workfile_save) + menu.install() + + register_event_callback("save", on_save) + register_event_callback("open", on_open) + register_event_callback("new", on_new) + register_event_callback("before.save", on_before_save) + register_event_callback("taskChanged", on_task_changed) + register_event_callback("workfile.save.before", before_workfile_save) + + def open_file(self, filepath): + return open_file(filepath) + + def save_file(self, filepath=None): + return save_file(filepath) + + def work_root(self, session): + return work_root(session) + + def current_file(self): + return current_file() + + def has_unsaved_changes(self): + return has_unsaved_changes() + + def file_extensions(self): + return file_extensions() + + def ls(self): + return ls() + + @contextlib.contextmanager + def maintained_selection(self): + with lib.maintained_selection(): + yield + + def _register_callbacks(self): + for handler, event in self._events.copy().items(): + if event is None: + continue + + try: + OpenMaya.MMessage.removeCallback(event) + self._events[handler] = None + except RuntimeError as exc: + self.log.info(exc) + + self._events[_on_scene_save] = OpenMaya.MSceneMessage.addCallback( + OpenMaya.MSceneMessage.kBeforeSave, _on_scene_save + ) + + self._events[_before_scene_save] = ( + OpenMaya.MSceneMessage.addCheckCallback( + OpenMaya.MSceneMessage.kBeforeSaveCheck, + _before_scene_save + ) + ) + + self._events[_on_scene_new] = OpenMaya.MSceneMessage.addCallback( + OpenMaya.MSceneMessage.kAfterNew, _on_scene_new + ) + + self._events[_on_maya_initialized] = ( + OpenMaya.MSceneMessage.addCallback( + OpenMaya.MSceneMessage.kMayaInitialized, + _on_maya_initialized + ) + ) + + self._events[_on_scene_open] = OpenMaya.MSceneMessage.addCallback( + OpenMaya.MSceneMessage.kAfterOpen, _on_scene_open + ) + + self.log.info("Installed event handler _on_scene_save..") + self.log.info("Installed event handler _before_scene_save..") + self.log.info("Installed event handler _on_scene_new..") + self.log.info("Installed event handler _on_maya_initialized..") + self.log.info("Installed event handler _on_scene_open..") def _set_project(): @@ -105,44 +188,6 @@ def _set_project(): cmds.workspace(workdir, openWorkspace=True) -def _register_callbacks(): - for handler, event in self._events.copy().items(): - if event is None: - continue - - try: - OpenMaya.MMessage.removeCallback(event) - self._events[handler] = None - except RuntimeError as e: - log.info(e) - - self._events[_on_scene_save] = OpenMaya.MSceneMessage.addCallback( - OpenMaya.MSceneMessage.kBeforeSave, _on_scene_save - ) - - self._events[_before_scene_save] = OpenMaya.MSceneMessage.addCheckCallback( - OpenMaya.MSceneMessage.kBeforeSaveCheck, _before_scene_save - ) - - self._events[_on_scene_new] = OpenMaya.MSceneMessage.addCallback( - OpenMaya.MSceneMessage.kAfterNew, _on_scene_new - ) - - self._events[_on_maya_initialized] = OpenMaya.MSceneMessage.addCallback( - OpenMaya.MSceneMessage.kMayaInitialized, _on_maya_initialized - ) - - self._events[_on_scene_open] = OpenMaya.MSceneMessage.addCallback( - OpenMaya.MSceneMessage.kAfterOpen, _on_scene_open - ) - - log.info("Installed event handler _on_scene_save..") - log.info("Installed event handler _before_scene_save..") - log.info("Installed event handler _on_scene_new..") - log.info("Installed event handler _on_maya_initialized..") - log.info("Installed event handler _on_scene_open..") - - def _on_maya_initialized(*args): emit_event("init") @@ -474,7 +519,6 @@ def on_task_changed(): workdir = legacy_io.Session["AVALON_WORKDIR"] if os.path.exists(workdir): log.info("Updating Maya workspace for task change to %s", workdir) - _set_project() # Set Maya fileDialog's start-dir to /scenes diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index a3ab483add..daaf305612 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,10 +1,11 @@ import os from openpype.api import get_project_settings from openpype.pipeline import install_host -from openpype.hosts.maya import api +from openpype.hosts.maya.api import MayaHostImplementation from maya import cmds -install_host(api) +host = MayaHostImplementation() +install_host(host) print("starting OpenPype usersetup") From df16589120736417862139467ed647c7e8286f11 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 14 Jun 2022 11:07:18 +0200 Subject: [PATCH 09/22] added interfaces to inheritance --- openpype/hosts/maya/api/pipeline.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 4924185f0a..f68bed9338 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -9,7 +9,7 @@ import maya.api.OpenMaya as om import pyblish.api from openpype.settings import get_project_settings -from openpype.host import HostImplementation +from openpype.host import HostImplementation, IWorkfileHost, ILoadHost import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( @@ -51,12 +51,12 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" -class MayaHostImplementation(HostImplementation): +class MayaHostImplementation(HostImplementation, IWorkfileHost, ILoadHost): name = "maya" def __init__(self): super(MayaHostImplementation, self).__init__() - self._events = {} + self._op_events = {} def install(self): project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) @@ -124,39 +124,39 @@ class MayaHostImplementation(HostImplementation): yield def _register_callbacks(self): - for handler, event in self._events.copy().items(): + for handler, event in self._op_events.copy().items(): if event is None: continue try: OpenMaya.MMessage.removeCallback(event) - self._events[handler] = None + self._op_events[handler] = None except RuntimeError as exc: self.log.info(exc) - self._events[_on_scene_save] = OpenMaya.MSceneMessage.addCallback( + self._op_events[_on_scene_save] = OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kBeforeSave, _on_scene_save ) - self._events[_before_scene_save] = ( + self._op_events[_before_scene_save] = ( OpenMaya.MSceneMessage.addCheckCallback( OpenMaya.MSceneMessage.kBeforeSaveCheck, _before_scene_save ) ) - self._events[_on_scene_new] = OpenMaya.MSceneMessage.addCallback( + self._op_events[_on_scene_new] = OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kAfterNew, _on_scene_new ) - self._events[_on_maya_initialized] = ( + self._op_events[_on_maya_initialized] = ( OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kMayaInitialized, _on_maya_initialized ) ) - self._events[_on_scene_open] = OpenMaya.MSceneMessage.addCallback( + self._op_events[_on_scene_open] = OpenMaya.MSceneMessage.addCallback( OpenMaya.MSceneMessage.kAfterOpen, _on_scene_open ) From 796348d4afc38a29eac73c97484d516eb5ba774b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Jun 2022 18:05:37 +0200 Subject: [PATCH 10/22] renamed 'HostImplementation' to 'HostBase' and `MayaHostImplementation' to 'MayaHost' --- openpype/host/__init__.py | 4 ++-- openpype/host/host.py | 6 +++--- openpype/hosts/maya/api/__init__.py | 4 ++-- openpype/hosts/maya/api/pipeline.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/host/__init__.py b/openpype/host/__init__.py index fcc9372a99..84a2fa930a 100644 --- a/openpype/host/__init__.py +++ b/openpype/host/__init__.py @@ -1,12 +1,12 @@ from .host import ( - HostImplementation, + HostBase, IWorkfileHost, ILoadHost, INewPublisher, ) __all__ = ( - "HostImplementation", + "HostBase", "IWorkfileHost", "ILoadHost", "INewPublisher", diff --git a/openpype/host/host.py b/openpype/host/host.py index 302c181598..2a59daf473 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -16,7 +16,7 @@ class MissingMethodsError(ValueError): @six.add_metaclass(ABCMeta) -class HostImplementation(object): +class HostBase(object): """Base of host implementation class. Host is pipeline implementation of DCC application. This class should help @@ -175,7 +175,7 @@ class ILoadHost: QUESTIONS - Is list container dependency of host or load plugins? - - Should this be directly in HostImplementation? + - Should this be directly in HostBase? - how to find out if referencing is available? - do we need to know that? """ @@ -369,7 +369,7 @@ class INewPublisher: but also some global data. At this moment are data related only to context publish plugins but that can extend in future. - HostImplementation does not have to inherit from this interface just have + HostBase does not have to inherit from this interface just have to imlement mentioned all listed methods. """ diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py index 6c28c59580..a6c5f50e1a 100644 --- a/openpype/hosts/maya/api/__init__.py +++ b/openpype/hosts/maya/api/__init__.py @@ -9,7 +9,7 @@ from .pipeline import ( ls, containerise, - MayaHostImplementation, + MayaHost, ) from .plugin import ( Creator, @@ -44,7 +44,7 @@ __all__ = [ "ls", "containerise", - "MayaHostImplementation", + "MayaHost", "Creator", "Loader", diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index f68bed9338..bfb9b289e0 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -9,7 +9,7 @@ import maya.api.OpenMaya as om import pyblish.api from openpype.settings import get_project_settings -from openpype.host import HostImplementation, IWorkfileHost, ILoadHost +from openpype.host import HostBase, IWorkfileHost, ILoadHost import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( @@ -51,11 +51,11 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") AVALON_CONTAINERS = ":AVALON_CONTAINERS" -class MayaHostImplementation(HostImplementation, IWorkfileHost, ILoadHost): +class MayaHost(HostBase, IWorkfileHost, ILoadHost): name = "maya" def __init__(self): - super(MayaHostImplementation, self).__init__() + super(MayaHost, self).__init__() self._op_events = {} def install(self): From 20cf87c07d08a058eff3684eeb8e0df0fae1fb1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 11:03:08 +0200 Subject: [PATCH 11/22] forgotten changes of MayaHost imports --- openpype/hosts/maya/startup/userSetup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index daaf305612..10e68c2ddb 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,10 +1,10 @@ import os from openpype.api import get_project_settings from openpype.pipeline import install_host -from openpype.hosts.maya.api import MayaHostImplementation +from openpype.hosts.maya.api import MayaHost from maya import cmds -host = MayaHostImplementation() +host = MayaHost() install_host(host) From ecd2686ad19af5ef9a47d197c2f439be6bc143bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 14:47:16 +0200 Subject: [PATCH 12/22] added some docstrings --- openpype/host/host.py | 95 ++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 2a59daf473..ab75ec5bc3 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -5,6 +5,13 @@ import six class MissingMethodsError(ValueError): + """Exception when host miss some required methods for specific workflow. + + Args: + host (HostBase): Host implementation where are missing methods. + missing_methods (list[str]): List of missing methods. + """ + def __init__(self, host, missing_methods): joined_missing = ", ".join( ['"{}"'.format(item) for item in missing_methods] @@ -53,15 +60,15 @@ class HostBase(object): install_host(host) ``` - # TODOs - - move content of 'install_host' as method of this class - - register host object - - install legacy_io - - install global plugin paths - - store registered plugin paths to this object - - handle current context (project, asset, task) - - this must be done in many separated steps - - have it's object of host tools instead of using globals + Todo: + - move content of 'install_host' as method of this class + - register host object + - install legacy_io + - install global plugin paths + - store registered plugin paths to this object + - handle current context (project, asset, task) + - this must be done in many separated steps + - have it's object of host tools instead of using globals This implementation will probably change over time when more functionality and responsibility will be added. @@ -75,7 +82,7 @@ class HostBase(object): Register DCC callbacks, host specific plugin paths, targets etc. (Part of what 'install' did in 'avalon' concept.) - NOTE: + Note: At this moment global "installation" must happen before host installation. Because of this current limitation it is recommended to implement 'install' method which is triggered after global @@ -127,10 +134,10 @@ class HostBase(object): Should return current context title if possible. - NOTE: This method is used only for UI purposes so it is possible to - return some logical title for contextless cases. - - Is not meant for "Context menu" label. + Note: + This method is used only for UI purposes so it is possible to + return some logical title for contextless cases. + Is not meant for "Context menu" label. Returns: str: Context title. @@ -159,6 +166,9 @@ class HostBase(object): This is DCC specific. Some may not allow to implement this ability that is reason why default implementation is empty context manager. + + Yields: + None: Yield when is ready to restore selected at the end. """ try: @@ -173,11 +183,11 @@ class ILoadHost: The load plugins can do referencing even without implementation of methods here, but switch and removement of containers would not be possible. - QUESTIONS - - Is list container dependency of host or load plugins? - - Should this be directly in HostBase? - - how to find out if referencing is available? - - do we need to know that? + Questions: + - Is list container dependency of host or load plugins? + - Should this be directly in HostBase? + - how to find out if referencing is available? + - do we need to know that? """ @staticmethod @@ -188,6 +198,9 @@ class ILoadHost: loading. Checks only existence of methods. Args: + HostBase: Object of host where to look for required methods. + + Returns: list[str]: Missing method implementations for loading workflow. """ @@ -202,6 +215,9 @@ class ILoadHost: def validate_load_methods(host): """Validate implemented methods of host for load workflow. + Args: + HostBase: Object of host to validate. + Raises: MissingMethodsError: If there are missing methods on host implementation. @@ -216,7 +232,7 @@ class ILoadHost: This can be implemented in hosts where referencing can be used. - TODO: + Todo: Rename function to something more self explanatory. Suggestion: 'get_referenced_containers' @@ -242,6 +258,9 @@ class IWorkfileHost: Method is used for validation of implemented functions related to workfiles. Checks only existence of methods. + Args: + HostBase: Object of host where to look for required methods. + Returns: list[str]: Missing method implementations for workfiles workflow. """ @@ -264,6 +283,9 @@ class IWorkfileHost: def validate_workfile_methods(host): """Validate implemented methods of host for workfiles workflow. + Args: + HostBase: Object of host to validate. + Raises: MissingMethodsError: If there are missing methods on host implementation. @@ -276,9 +298,10 @@ class IWorkfileHost: def file_extensions(self): """Extensions that can be used as save. - QUESTION: This could potentially use 'HostDefinition'. + Questions: + This could potentially use 'HostDefinition'. - TODO: + Todo: Rename to 'get_workfile_extensions'. """ @@ -288,7 +311,7 @@ class IWorkfileHost: def save_file(self, dst_path=None): """Save currently opened scene. - TODO: + Todo: Rename to 'save_current_workfile'. Args: @@ -302,7 +325,7 @@ class IWorkfileHost: def open_file(self, filepath): """Open passed filepath in the host. - TODO: + Todo: Rename to 'open_workfile'. Args: @@ -315,7 +338,7 @@ class IWorkfileHost: def current_file(self): """Retreive path to current opened file. - TODO: + Todo: Rename to 'get_current_workfile'. Returns: @@ -342,16 +365,16 @@ class IWorkfileHost: def work_root(self, session): """Modify workdir per host. - WARNING: - We must handle this modification with more sofisticated way because - this can't be called out of DCC so opening of last workfile - (calculated before DCC is launched) is complicated. Also breaking - defined work template is not a good idea. - Only place where it's really used and can make sense is Maya. There - workspace.mel can modify subfolders where to look for maya files. - Default implementation keeps workdir untouched. + Warnings: + We must handle this modification with more sofisticated way because + this can't be called out of DCC so opening of last workfile + (calculated before DCC is launched) is complicated. Also breaking + defined work template is not a good idea. + Only place where it's really used and can make sense is Maya. There + workspace.mel can modify subfolders where to look for maya files. + Args: session (dict): Session context data. @@ -381,6 +404,9 @@ class INewPublisher: new publish creation. Checks only existence of methods. Args: + HostBase: Object of host where to look for required methods. + + Returns: list[str]: Missing method implementations for new publsher workflow. """ @@ -399,6 +425,9 @@ class INewPublisher: def validate_publish_methods(host): """Validate implemented methods of host for create-publish workflow. + Args: + HostBase: Object of host to validate. + Raises: MissingMethodsError: If there are missing methods on host implementation. From e36c80fd09adea0322bc03c3c4da1764cc620d62 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 15:41:41 +0200 Subject: [PATCH 13/22] added type hints --- openpype/host/host.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/host/host.py b/openpype/host/host.py index ab75ec5bc3..676f0e6771 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -3,6 +3,9 @@ import contextlib from abc import ABCMeta, abstractproperty, abstractmethod import six +# NOTE can't import 'typing' because of issues in Maya 2020 +# - shiboken crashes on 'typing' module import + class MissingMethodsError(ValueError): """Exception when host miss some required methods for specific workflow. @@ -77,6 +80,7 @@ class HostBase(object): _log = None def __init__(self): + # type: () -> None """Initialization of host. Register DCC callbacks, host specific plugin paths, targets etc. @@ -93,17 +97,20 @@ class HostBase(object): @property def log(self): + # type: () -> logging.Logger if self._log is None: self._log = logging.getLogger(self.__class__.__name__) return self._log @abstractproperty def name(self): + # type: () -> str """Host implementation name.""" pass def get_current_context(self): + # type: () -> Mapping[str, Union[str, None]] """Get current context information. This method should be used to get current context of host. Usage of @@ -130,6 +137,7 @@ class HostBase(object): } def get_context_title(self): + # type: () -> Union[str, None] """Context title shown for UI purposes. Should return current context title if possible. @@ -162,6 +170,7 @@ class HostBase(object): @contextlib.contextmanager def maintained_selection(self): + # type: () -> None """Some functionlity will happen but selection should stay same. This is DCC specific. Some may not allow to implement this ability @@ -192,6 +201,7 @@ class ILoadHost: @staticmethod def get_missing_load_methods(host): + # type: (HostBase) -> List[str] """Look for missing methods on host implementation. Method is used for validation of implemented functions related to @@ -213,6 +223,7 @@ class ILoadHost: @staticmethod def validate_load_methods(host): + # type: (HostBase) -> None """Validate implemented methods of host for load workflow. Args: @@ -228,6 +239,7 @@ class ILoadHost: @abstractmethod def ls(self): + # type: (HostBase) -> List[Mapping[str, Any]] """Retreive referenced containers from scene. This can be implemented in hosts where referencing can be used. @@ -253,6 +265,7 @@ class IWorkfileHost: @staticmethod def get_missing_workfile_methods(host): + # type: (HostBase) -> List[str] """Look for missing methods on host implementation. Method is used for validation of implemented functions related to @@ -281,6 +294,7 @@ class IWorkfileHost: @staticmethod def validate_workfile_methods(host): + # type: (HostBase) -> None """Validate implemented methods of host for workfiles workflow. Args: @@ -296,6 +310,7 @@ class IWorkfileHost: @abstractmethod def file_extensions(self): + # type: () -> List[str] """Extensions that can be used as save. Questions: @@ -309,6 +324,7 @@ class IWorkfileHost: @abstractmethod def save_file(self, dst_path=None): + # type: (Optional[str]) -> None """Save currently opened scene. Todo: @@ -323,6 +339,7 @@ class IWorkfileHost: @abstractmethod def open_file(self, filepath): + # type: (str) -> None """Open passed filepath in the host. Todo: @@ -336,6 +353,7 @@ class IWorkfileHost: @abstractmethod def current_file(self): + # type: () -> Union[str, None] """Retreive path to current opened file. Todo: @@ -349,6 +367,7 @@ class IWorkfileHost: return None def has_unsaved_changes(self): + # type: () -> Union[bool, None] """Currently opened scene is saved. Not all hosts can know if current scene is saved because the API of @@ -363,6 +382,7 @@ class IWorkfileHost: return None def work_root(self, session): + # type: (Mapping[str, str]) -> str """Modify workdir per host. Default implementation keeps workdir untouched. @@ -398,6 +418,7 @@ class INewPublisher: @staticmethod def get_missing_publish_methods(host): + # type: (HostBase) -> List[str] """Look for missing methods on host implementation. Method is used for validation of implemented functions related to @@ -423,6 +444,7 @@ class INewPublisher: @staticmethod def validate_publish_methods(host): + # type: (HostBase) -> None """Validate implemented methods of host for create-publish workflow. Args: @@ -438,6 +460,7 @@ class INewPublisher: @abstractmethod def get_context_data(self): + # type: () -> Mapping[str, Any] """Get global data related to creation-publishing from workfile. These data are not related to any created instance but to whole @@ -455,6 +478,7 @@ class INewPublisher: @abstractmethod def update_context_data(self, data, changes): + # type: (Mapping[str, Any], Mapping[str, Any]) -> None """Store global context data to workfile. Called when some values in context data has changed. From bf592641e870556c474da160b1e107a3377388a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 17:13:29 +0200 Subject: [PATCH 14/22] added new names of methods and old variants are marked as deprecated --- openpype/host/host.py | 142 ++++++++++++++++++---------- openpype/hosts/maya/api/pipeline.py | 12 +-- 2 files changed, 100 insertions(+), 54 deletions(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 676f0e6771..1755c19216 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -105,7 +105,7 @@ class HostBase(object): @abstractproperty def name(self): # type: () -> str - """Host implementation name.""" + """Host name.""" pass @@ -201,19 +201,22 @@ class ILoadHost: @staticmethod def get_missing_load_methods(host): - # type: (HostBase) -> List[str] - """Look for missing methods on host implementation. + # type: (Union[ModuleType, HostBase]) -> List[str] + """Look for missing methods on "old type" host implementation. Method is used for validation of implemented functions related to loading. Checks only existence of methods. Args: - HostBase: Object of host where to look for required methods. + Union[ModuleType, HostBase]: Object of host where to look for required methods. Returns: list[str]: Missing method implementations for loading workflow. """ + if isinstance(host, ILoadHost): + return [] + required = ["ls"] missing = [] for name in required: @@ -223,11 +226,11 @@ class ILoadHost: @staticmethod def validate_load_methods(host): - # type: (HostBase) -> None - """Validate implemented methods of host for load workflow. + # type: (Union[ModuleType, HostBase]) -> None + """Validate implemented methods of "old type" host for load workflow. Args: - HostBase: Object of host to validate. + Union[ModuleType, HostBase]: Object of host to validate. Raises: MissingMethodsError: If there are missing methods on host @@ -238,8 +241,8 @@ class ILoadHost: raise MissingMethodsError(host, missing) @abstractmethod - def ls(self): - # type: (HostBase) -> List[Mapping[str, Any]] + def get_referenced_containers(self): + # type: () -> List[Mapping[str, Any]] """Retreive referenced containers from scene. This can be implemented in hosts where referencing can be used. @@ -251,33 +254,42 @@ class ILoadHost: Returns: list[dict]: Information about loaded containers. """ - return [] + + pass + + # --- Deprecated method names --- + def ls(self): + """Deprecated variant of 'get_referenced_containers'. + + Todo: + Remove when all usages are replaced. + """ + + return self.get_referenced_containers() @six.add_metaclass(ABCMeta) class IWorkfileHost: - """Implementation requirements to be able use workfile utils and tool. - - This interface just provides what is needed to implement in host - implementation to support workfiles workflow, but does not have necessarily - to inherit from this interface. - """ + """Implementation requirements to be able use workfile utils and tool.""" @staticmethod def get_missing_workfile_methods(host): - # type: (HostBase) -> List[str] - """Look for missing methods on host implementation. + # type: (Union[ModuleType, HostBase]) -> List[str] + """Look for missing methods on "old type" host implementation. Method is used for validation of implemented functions related to workfiles. Checks only existence of methods. Args: - HostBase: Object of host where to look for required methods. + Union[ModuleType, HostBase]: Object of host where to look for required methods. Returns: list[str]: Missing method implementations for workfiles workflow. """ + if isinstance(host, IWorkfileHost): + return [] + required = [ "open_file", "save_file", @@ -294,42 +306,37 @@ class IWorkfileHost: @staticmethod def validate_workfile_methods(host): - # type: (HostBase) -> None - """Validate implemented methods of host for workfiles workflow. + # type: (Union[ModuleType, HostBase]) -> None + """Validate methods of "old type" host for workfiles workflow. Args: - HostBase: Object of host to validate. + Union[ModuleType, HostBase]: Object of host to validate. Raises: MissingMethodsError: If there are missing methods on host implementation. """ + missing = IWorkfileHost.get_missing_workfile_methods(host) if missing: raise MissingMethodsError(host, missing) @abstractmethod - def file_extensions(self): + def get_workfile_extensions(self): # type: () -> List[str] """Extensions that can be used as save. Questions: This could potentially use 'HostDefinition'. - - Todo: - Rename to 'get_workfile_extensions'. """ return [] @abstractmethod - def save_file(self, dst_path=None): + def save_current_workfile(self, dst_path=None): # type: (Optional[str]) -> None """Save currently opened scene. - Todo: - Rename to 'save_current_workfile'. - Args: dst_path (str): Where the current scene should be saved. Or use current path if 'None' is passed. @@ -338,13 +345,10 @@ class IWorkfileHost: pass @abstractmethod - def open_file(self, filepath): + def open_workfile(self, filepath): # type: (str) -> None """Open passed filepath in the host. - Todo: - Rename to 'open_workfile'. - Args: filepath (str): Path to workfile. """ @@ -352,13 +356,10 @@ class IWorkfileHost: pass @abstractmethod - def current_file(self): + def get_current_workfile(self): # type: () -> Union[str, None] """Retreive path to current opened file. - Todo: - Rename to 'get_current_workfile'. - Returns: str: Path to file which is currently opened. None: If nothing is opened. @@ -366,7 +367,7 @@ class IWorkfileHost: return None - def has_unsaved_changes(self): + def workfile_has_unsaved_changes(self): # type: () -> Union[bool, None] """Currently opened scene is saved. @@ -404,6 +405,51 @@ class IWorkfileHost: return session["AVALON_WORKDIR"] + # --- Deprecated method names --- + def file_extensions(self): + """Deprecated variant of 'get_workfile_extensions'. + + Todo: + Remove when all usages are replaced. + """ + return self.get_workfile_extensions() + + def save_file(self, dst_path=None): + """Deprecated variant of 'save_current_workfile'. + + Todo: + Remove when all usages are replaced. + """ + + self.save_current_workfile() + + def open_file(self, filepath): + """Deprecated variant of 'open_workfile'. + + Todo: + Remove when all usages are replaced. + """ + + return self.open_workfile(filepath) + + def current_file(self): + """Deprecated variant of 'get_current_workfile'. + + Todo: + Remove when all usages are replaced. + """ + + return self.get_current_workfile() + + def has_unsaved_changes(self): + """Deprecated variant of 'workfile_has_unsaved_changes'. + + Todo: + Remove when all usages are replaced. + """ + + return self.workfile_has_unsaved_changes() + class INewPublisher: """Functions related to new creation system in new publisher. @@ -411,27 +457,27 @@ class INewPublisher: New publisher is not storing information only about each created instance but also some global data. At this moment are data related only to context publish plugins but that can extend in future. - - HostBase does not have to inherit from this interface just have - to imlement mentioned all listed methods. """ @staticmethod def get_missing_publish_methods(host): - # type: (HostBase) -> List[str] - """Look for missing methods on host implementation. + # type: (Union[ModuleType, HostBase]) -> List[str] + """Look for missing methods on "old type" host implementation. Method is used for validation of implemented functions related to new publish creation. Checks only existence of methods. Args: - HostBase: Object of host where to look for required methods. + Union[ModuleType, HostBase]: Host module where to look for required methods. Returns: list[str]: Missing method implementations for new publsher workflow. """ + if isinstance(host, INewPublisher): + return [] + required = [ "get_context_data", "update_context_data", @@ -444,11 +490,11 @@ class INewPublisher: @staticmethod def validate_publish_methods(host): - # type: (HostBase) -> None - """Validate implemented methods of host for create-publish workflow. + # type: (Union[ModuleType, HostBase]) -> None + """Validate implemented methods of "old type" host. Args: - HostBase: Object of host to validate. + Union[ModuleType, HostBase]: Host module to validate. Raises: MissingMethodsError: If there are missing methods on host diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index bfb9b289e0..b8c6042e4f 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -97,25 +97,25 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost): register_event_callback("taskChanged", on_task_changed) register_event_callback("workfile.save.before", before_workfile_save) - def open_file(self, filepath): + def open_workfile(self, filepath): return open_file(filepath) - def save_file(self, filepath=None): + def save_current_workfile(self, filepath=None): return save_file(filepath) def work_root(self, session): return work_root(session) - def current_file(self): + def get_current_workfile(self): return current_file() - def has_unsaved_changes(self): + def workfile_has_unsaved_changes(self): return has_unsaved_changes() - def file_extensions(self): + def get_workfile_extensions(self): return file_extensions() - def ls(self): + def get_referenced_containers(self): return ls() @contextlib.contextmanager From 779f4230bcaa6f499d1e293baf6cd6d964a15644 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 17:16:55 +0200 Subject: [PATCH 15/22] use new method names based on inheritance in workfiles tool and sceneinventory --- openpype/tools/sceneinventory/model.py | 6 +++- openpype/tools/workfiles/files_widget.py | 44 +++++++++++++++++++----- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 0bb9c4a658..2894932e96 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -6,6 +6,7 @@ from collections import defaultdict from Qt import QtCore, QtGui import qtawesome +from openpype.host import ILoadHost from openpype.client import ( get_asset_by_id, get_subset_by_id, @@ -193,7 +194,10 @@ class InventoryModel(TreeModel): host = registered_host() if not items: # for debugging or testing, injecting items from outside - items = host.ls() + if isinstance(host, ILoadHost): + items = host.get_referenced_containers() + else: + items = host.ls() self.clear() diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index a7e54471dc..8669a28f6c 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -6,6 +6,7 @@ import copy import Qt from Qt import QtWidgets, QtCore +from openpype.host import IWorkfileHost from openpype.client import get_asset_by_id from openpype.tools.utils import PlaceholderLineEdit from openpype.tools.utils.delegates import PrettyTimeDelegate @@ -125,7 +126,7 @@ class FilesWidget(QtWidgets.QWidget): filter_layout.addWidget(published_checkbox, 0) # Create the Files models - extensions = set(self.host.file_extensions()) + extensions = set(self.self._get_host_extensions()) views_widget = QtWidgets.QWidget(self) # --- Workarea view --- @@ -452,7 +453,12 @@ class FilesWidget(QtWidgets.QWidget): def open_file(self, filepath): host = self.host - if host.has_unsaved_changes(): + if isinstance(host, IWorkfileHost): + has_unsaved_changes = host.workfile_has_unsaved_changes() + else: + has_unsaved_changes = host.has_unsaved_changes() + + if has_unsaved_changes: result = self.save_changes_prompt() if result is None: # Cancel operation @@ -460,7 +466,10 @@ class FilesWidget(QtWidgets.QWidget): # Save first if has changes if result: - current_file = host.current_file() + if isinstance(host, IWorkfileHost): + current_file = host.get_current_workfile() + else: + current_file = host.current_file() if not current_file: # If the user requested to save the current scene # we can't actually automatically do so if the current @@ -471,7 +480,10 @@ class FilesWidget(QtWidgets.QWidget): return # Save current scene, continue to open file - host.save_file(current_file) + if isinstance(host, IWorkfileHost): + host.save_current_workfile(current_file) + else: + host.save_file(current_file) event_data_before = self._get_event_context_data() event_data_before["filepath"] = filepath @@ -482,7 +494,10 @@ class FilesWidget(QtWidgets.QWidget): source="workfiles.tool" ) self._enter_session() - host.open_file(filepath) + if isinstance(host, IWorkfileHost): + host.open_workfile(filepath) + else: + host.open_file(filepath) emit_event( "workfile.open.after", event_data_after, @@ -524,7 +539,7 @@ class FilesWidget(QtWidgets.QWidget): filepath = self._get_selected_filepath() extensions = [os.path.splitext(filepath)[1]] else: - extensions = self.host.file_extensions() + extensions = self._get_host_extensions() window = SaveAsDialog( parent=self, @@ -572,9 +587,14 @@ class FilesWidget(QtWidgets.QWidget): self.open_file(path) + def _get_host_extensions(self): + if isinstance(self.host, IWorkfileHost): + return self.host.get_workfile_extensions() + return self.host.file_extensions() + def on_browse_pressed(self): ext_filter = "Work File (*{0})".format( - " *".join(self.host.file_extensions()) + " *".join(self._get_host_extensions()) ) kwargs = { "caption": "Work Files", @@ -632,10 +652,16 @@ class FilesWidget(QtWidgets.QWidget): self._enter_session() if not self.published_enabled: - self.host.save_file(filepath) + if isinstance(self.host, IWorkfileHost): + self.host.save_current_workfile(filepath) + else: + self.host.save_file(filepath) else: shutil.copy(src_path, filepath) - self.host.open_file(filepath) + if isinstance(self.host, IWorkfileHost): + self.host.open_workfile(filepath) + else: + self.host.open_file(filepath) # Create extra folders create_workdir_extra_folders( From 3ab8b75ada8c5bb56359903928b78ff281ba5fad Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 17:18:25 +0200 Subject: [PATCH 16/22] fix double accessed self --- openpype/tools/workfiles/files_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index 8669a28f6c..c019518d8e 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -126,7 +126,7 @@ class FilesWidget(QtWidgets.QWidget): filter_layout.addWidget(published_checkbox, 0) # Create the Files models - extensions = set(self.self._get_host_extensions()) + extensions = set(self._get_host_extensions()) views_widget = QtWidgets.QWidget(self) # --- Workarea view --- From fcefcc74348f7f7be89c3d34caa4ca88819dbd1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 24 Jun 2022 17:52:29 +0200 Subject: [PATCH 17/22] removed type hints --- openpype/host/host.py | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 1755c19216..94eeeb986f 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -80,7 +80,6 @@ class HostBase(object): _log = None def __init__(self): - # type: () -> None """Initialization of host. Register DCC callbacks, host specific plugin paths, targets etc. @@ -97,20 +96,17 @@ class HostBase(object): @property def log(self): - # type: () -> logging.Logger if self._log is None: self._log = logging.getLogger(self.__class__.__name__) return self._log @abstractproperty def name(self): - # type: () -> str """Host name.""" pass def get_current_context(self): - # type: () -> Mapping[str, Union[str, None]] """Get current context information. This method should be used to get current context of host. Usage of @@ -137,7 +133,6 @@ class HostBase(object): } def get_context_title(self): - # type: () -> Union[str, None] """Context title shown for UI purposes. Should return current context title if possible. @@ -170,7 +165,6 @@ class HostBase(object): @contextlib.contextmanager def maintained_selection(self): - # type: () -> None """Some functionlity will happen but selection should stay same. This is DCC specific. Some may not allow to implement this ability @@ -201,14 +195,14 @@ class ILoadHost: @staticmethod def get_missing_load_methods(host): - # type: (Union[ModuleType, HostBase]) -> List[str] """Look for missing methods on "old type" host implementation. Method is used for validation of implemented functions related to loading. Checks only existence of methods. Args: - Union[ModuleType, HostBase]: Object of host where to look for required methods. + Union[ModuleType, HostBase]: Object of host where to look for + required methods. Returns: list[str]: Missing method implementations for loading workflow. @@ -226,7 +220,6 @@ class ILoadHost: @staticmethod def validate_load_methods(host): - # type: (Union[ModuleType, HostBase]) -> None """Validate implemented methods of "old type" host for load workflow. Args: @@ -242,7 +235,6 @@ class ILoadHost: @abstractmethod def get_referenced_containers(self): - # type: () -> List[Mapping[str, Any]] """Retreive referenced containers from scene. This can be implemented in hosts where referencing can be used. @@ -274,14 +266,14 @@ class IWorkfileHost: @staticmethod def get_missing_workfile_methods(host): - # type: (Union[ModuleType, HostBase]) -> List[str] """Look for missing methods on "old type" host implementation. Method is used for validation of implemented functions related to workfiles. Checks only existence of methods. Args: - Union[ModuleType, HostBase]: Object of host where to look for required methods. + Union[ModuleType, HostBase]: Object of host where to look for + required methods. Returns: list[str]: Missing method implementations for workfiles workflow. @@ -306,7 +298,6 @@ class IWorkfileHost: @staticmethod def validate_workfile_methods(host): - # type: (Union[ModuleType, HostBase]) -> None """Validate methods of "old type" host for workfiles workflow. Args: @@ -323,7 +314,6 @@ class IWorkfileHost: @abstractmethod def get_workfile_extensions(self): - # type: () -> List[str] """Extensions that can be used as save. Questions: @@ -334,7 +324,6 @@ class IWorkfileHost: @abstractmethod def save_current_workfile(self, dst_path=None): - # type: (Optional[str]) -> None """Save currently opened scene. Args: @@ -346,7 +335,6 @@ class IWorkfileHost: @abstractmethod def open_workfile(self, filepath): - # type: (str) -> None """Open passed filepath in the host. Args: @@ -357,7 +345,6 @@ class IWorkfileHost: @abstractmethod def get_current_workfile(self): - # type: () -> Union[str, None] """Retreive path to current opened file. Returns: @@ -368,7 +355,6 @@ class IWorkfileHost: return None def workfile_has_unsaved_changes(self): - # type: () -> Union[bool, None] """Currently opened scene is saved. Not all hosts can know if current scene is saved because the API of @@ -383,7 +369,6 @@ class IWorkfileHost: return None def work_root(self, session): - # type: (Mapping[str, str]) -> str """Modify workdir per host. Default implementation keeps workdir untouched. @@ -461,14 +446,14 @@ class INewPublisher: @staticmethod def get_missing_publish_methods(host): - # type: (Union[ModuleType, HostBase]) -> List[str] """Look for missing methods on "old type" host implementation. Method is used for validation of implemented functions related to new publish creation. Checks only existence of methods. Args: - Union[ModuleType, HostBase]: Host module where to look for required methods. + Union[ModuleType, HostBase]: Host module where to look for + required methods. Returns: list[str]: Missing method implementations for new publsher @@ -490,7 +475,6 @@ class INewPublisher: @staticmethod def validate_publish_methods(host): - # type: (Union[ModuleType, HostBase]) -> None """Validate implemented methods of "old type" host. Args: @@ -506,7 +490,6 @@ class INewPublisher: @abstractmethod def get_context_data(self): - # type: () -> Mapping[str, Any] """Get global data related to creation-publishing from workfile. These data are not related to any created instance but to whole @@ -524,7 +507,6 @@ class INewPublisher: @abstractmethod def update_context_data(self, data, changes): - # type: (Mapping[str, Any], Mapping[str, Any]) -> None """Store global context data to workfile. Called when some values in context data has changed. From b4d141f1a5e7ebc04e055b0d376f4433699112cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 24 Jun 2022 17:53:19 +0200 Subject: [PATCH 18/22] initial commit of docstrings --- website/docs/dev_host_implementation.md | 74 +++++++++++++++++++++++++ website/sidebars.js | 1 + 2 files changed, 75 insertions(+) create mode 100644 website/docs/dev_host_implementation.md diff --git a/website/docs/dev_host_implementation.md b/website/docs/dev_host_implementation.md new file mode 100644 index 0000000000..e3a2fff5e2 --- /dev/null +++ b/website/docs/dev_host_implementation.md @@ -0,0 +1,74 @@ +--- +id: dev_host_implementation +title: Host implementation +sidebar_label: Host implementation +toc_max_heading_level: 4 +--- + +Host is an integration of DCC but in most of cases have logic that need to be handled before DCC is launched. Then based on abilities (or purpose) of DCC the integration can support different pipeline workflows. + +## Pipeline workflows +Workflows available in OpenPype are Workfiles, Load and Create-Publish. Each of them may require some functionality available in integration (e.g. call host API to achieve certain functionality). We'll go through them later. + +## How to implement and manage host +At this moment there is not fully unified way how host should be implemented but we're working on it. Host should have a "public face" code that can be used outside of DCC and in-DCC integration code. The main reason is that in-DCC code can have specific dependencies for python modules not available out of it's process. Hosts are located in `openpype/hosts/{host name}` folder. Current code (at many places) expect that the host name has equivalent folder there. So each subfolder should be named with the name of host it represents. + +### Recommended folder structure +``` +openpype/hosts/{host name} +│ +│ # Content of DCC integration - with in-DCC imports +├─ api +│ ├─ __init__.py +│ └─ [DCC integration files] +│ +│ # Plugins related to host - dynamically imported (can contain in-DCC imports) +├─ plugins +│ ├─ create +│ │ └─ [create plugin files] +│ ├─ load +│ │ └─ [load plugin files] +│ └─ publish +│ └─ [publish plugin files] +│ +│ # Launch hooks - used to modify how application is launched +├─ hooks +│ └─ [some pre/post launch hooks] +| +│ # Code initializing host integration in-DCC (DCC specific - example from Maya) +├─ startup +│ └─ userSetup.py +│ +│ # Public interface +├─ __init__.py +└─ [other public code] +``` + +### Launch Hooks +Launch hooks are not directly connected to host implementation, but they can be used to modify launch of process which may be crutial for the implementation. Launch hook are plugins called when DCC is launched. They are processed in sequence before and after launch. Pre launch hooks can change how process of DCC is launched, e.g. change subprocess flags, modify environments or modify launch arguments. If prelaunch hook crashes the application is not launched at all. Postlaunch hooks are triggered after launch of subprocess. They can be used to change statuses in your project tracker, start timer, etc. Crashed postlaunch hooks have no effect on rest of postlaunch hooks or launched process. They can be filtered by platform, host and application and order is defined by integer value. Hooks inside host are automatically loaded (one reason why folder name should match host name) or can be defined from modules. Hooks execution share same launch context where can be stored data used across multiple hooks (please be very specific in stored keys e.g. 'project' vs. 'project_name'). For more detailed information look into `openpype/lib/applications.py`. + +### Public interface +Public face is at this moment related to launching of the DCC. At this moment there there is only option to modify environment variables before launch by implementing function `add_implementation_envs` (must be available in `openpype/hosts/{host name}/__init__.py`). The function is called after pre launch hooks, as last step before subprocess launch, to be able set environment variables crutial for proper integration. It is also good place for functions that are used in prelaunch hooks and in-DCC integration. Future plans are to be able get workfiles extensions from here. Right now workfiles extensions are hardcoded in `openpype/pipeline/constants.py` under `HOST_WORKFILE_EXTENSIONS`, we would like to handle hosts as addons similar to OpenPype modules, and more improvements which are now hardcoded. + +### Integration +We've prepared base class `HostBase` in `openpype/host/host.py` to define minimum requirements and provide some default implementations. The minimum requirement for a host is `name` attribute, this host would not be able to do much but is valid. To extend functionality we've prepared interfaces that helps to identify what is host capable of and if is possible to use certain tools with it. For those cases we defined interfaces for each workflow. `IWorkfileHost` interface add requirement to implement workfiles related methods which makes host usable in combination with Workfiles tool. `ILoadHost` interface add requirements to be able load, update, switch or remove referenced representations which should add support to use Loader and Scene Inventory tools. `INewPublisher` interface is required to be able use host with new OpenPype publish workflow. This is what must or can be implemented to allow certain functionality. `HostBase` will have more responsibility which will be taken from global variables, this process won't happen at once but will be slow to keep backwards compatibility for some time. + +#### Example +```python +from openpype.host import HostBase, IWorkfileHost, ILoadHost + + +class MayaHost(HostBase, IWorkfileHost, ILoadHost): + def open_workfile(self, filepath): + ... + + def save_current_workfile(self, filepath=None): + ... + + def get_current_workfile(self): + ... + ... +``` + +### Install integration +We have host class, now where and how to initialize it. diff --git a/website/sidebars.js b/website/sidebars.js index d4fec9fba2..0e578bd085 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -155,6 +155,7 @@ module.exports = { type: "category", label: "Hosts integrations", items: [ + "dev_host_implementation", "dev_publishing" ] } From 6af7f906e5b3cde5d76aac36676e2a3ee3f18ac6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 13:19:15 +0200 Subject: [PATCH 19/22] added host installation and usage of tools in host --- website/docs/dev_host_implementation.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/website/docs/dev_host_implementation.md b/website/docs/dev_host_implementation.md index e3a2fff5e2..3702483ad1 100644 --- a/website/docs/dev_host_implementation.md +++ b/website/docs/dev_host_implementation.md @@ -14,7 +14,7 @@ Workflows available in OpenPype are Workfiles, Load and Create-Publish. Each of At this moment there is not fully unified way how host should be implemented but we're working on it. Host should have a "public face" code that can be used outside of DCC and in-DCC integration code. The main reason is that in-DCC code can have specific dependencies for python modules not available out of it's process. Hosts are located in `openpype/hosts/{host name}` folder. Current code (at many places) expect that the host name has equivalent folder there. So each subfolder should be named with the name of host it represents. ### Recommended folder structure -``` +```python openpype/hosts/{host name} │ │ # Content of DCC integration - with in-DCC imports @@ -51,7 +51,7 @@ Launch hooks are not directly connected to host implementation, but they can be Public face is at this moment related to launching of the DCC. At this moment there there is only option to modify environment variables before launch by implementing function `add_implementation_envs` (must be available in `openpype/hosts/{host name}/__init__.py`). The function is called after pre launch hooks, as last step before subprocess launch, to be able set environment variables crutial for proper integration. It is also good place for functions that are used in prelaunch hooks and in-DCC integration. Future plans are to be able get workfiles extensions from here. Right now workfiles extensions are hardcoded in `openpype/pipeline/constants.py` under `HOST_WORKFILE_EXTENSIONS`, we would like to handle hosts as addons similar to OpenPype modules, and more improvements which are now hardcoded. ### Integration -We've prepared base class `HostBase` in `openpype/host/host.py` to define minimum requirements and provide some default implementations. The minimum requirement for a host is `name` attribute, this host would not be able to do much but is valid. To extend functionality we've prepared interfaces that helps to identify what is host capable of and if is possible to use certain tools with it. For those cases we defined interfaces for each workflow. `IWorkfileHost` interface add requirement to implement workfiles related methods which makes host usable in combination with Workfiles tool. `ILoadHost` interface add requirements to be able load, update, switch or remove referenced representations which should add support to use Loader and Scene Inventory tools. `INewPublisher` interface is required to be able use host with new OpenPype publish workflow. This is what must or can be implemented to allow certain functionality. `HostBase` will have more responsibility which will be taken from global variables, this process won't happen at once but will be slow to keep backwards compatibility for some time. +We've prepared base class `HostBase` in `openpype/host/host.py` to define minimum requirements and provide some default method implementations. The minimum requirement for a host is `name` attribute, this host would not be able to do much but is valid. To extend functionality we've prepared interfaces that helps to identify what is host capable of and if is possible to use certain tools with it. For those cases we defined interfaces for each workflow. `IWorkfileHost` interface add requirement to implement workfiles related methods which makes host usable in combination with Workfiles tool. `ILoadHost` interface add requirements to be able load, update, switch or remove referenced representations which should add support to use Loader and Scene Inventory tools. `INewPublisher` interface is required to be able use host with new OpenPype publish workflow. This is what must or can be implemented to allow certain functionality. `HostBase` will have more responsibility which will be taken from global variables in future. This process won't happen at once, but will be slow to keep backwards compatibility for some time. #### Example ```python @@ -71,4 +71,19 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost): ``` ### Install integration -We have host class, now where and how to initialize it. +We have prepared a host class, now where and how to initialize it's object? This part is DCC specific. In DCCs like Maya with embedded python and Qt we use advantage of being able to initialize object of the class directly in DCC process on start, the same happens in Nuke, Hiero and Houdini. In DCCs like Photoshop or Harmony there is launched OpenPype (python) process next to it which handles host initialization and communication with the DCC process (e.g. using sockects). Created object of host must be installed and registered to global scope of OpenPype. Which means that at this moment one process can handle only one host at a time. + +#### Install example (Maya startup file) +```python +from openpype.pipeline import install_host +from openpype.hosts.maya.api import MayaHost + + +host = MayaHost() +install_host(host) +``` + +Function `install_host` cares about installing global plugins, callbacks and register host. Host registration means that the object is kept in memory and is accessible using `get_registered_host()`. + +### Using UI tools +Most of functionality in DCCs is provided to artists by using UI tools. We're trying to keep UIs consistent so we use same set of tools in each host, all or most of them are Qt based. There is a `HostToolsHelper` in `openpype/tools/utils/host_tools.py` which unify showing of default tools, they can be showed almost at any point. Some of them are validating if host is capable of using them (Workfiles, Loader and Scene Inventory) which is related to [pipeline workflows](#pipeline-workflows). `HostToolsHelper` provides API to show tools but host integration must care about giving artists ability to show them. Most of DCCs have some extendable menu bar where is possible to add custom actions, which is preferred approach how to give ability to show the tools. From 96218779a2f602be9f4196f2c33ca3178839153b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 14:06:58 +0200 Subject: [PATCH 20/22] renamed 'save_current_workfile' -> 'save_workfile' --- openpype/host/host.py | 6 +++--- openpype/hosts/maya/api/pipeline.py | 2 +- openpype/tools/workfiles/files_widget.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 94eeeb986f..b7e31d0854 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -323,7 +323,7 @@ class IWorkfileHost: return [] @abstractmethod - def save_current_workfile(self, dst_path=None): + def save_workfile(self, dst_path=None): """Save currently opened scene. Args: @@ -400,13 +400,13 @@ class IWorkfileHost: return self.get_workfile_extensions() def save_file(self, dst_path=None): - """Deprecated variant of 'save_current_workfile'. + """Deprecated variant of 'save_workfile'. Todo: Remove when all usages are replaced. """ - self.save_current_workfile() + self.save_workfile() def open_file(self, filepath): """Deprecated variant of 'open_workfile'. diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index b8c6042e4f..bad77f00c9 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -100,7 +100,7 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost): def open_workfile(self, filepath): return open_file(filepath) - def save_current_workfile(self, filepath=None): + def save_workfile(self, filepath=None): return save_file(filepath) def work_root(self, session): diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index c019518d8e..48ab0fc66e 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -481,7 +481,7 @@ class FilesWidget(QtWidgets.QWidget): # Save current scene, continue to open file if isinstance(host, IWorkfileHost): - host.save_current_workfile(current_file) + host.save_workfile(current_file) else: host.save_file(current_file) @@ -653,7 +653,7 @@ class FilesWidget(QtWidgets.QWidget): if not self.published_enabled: if isinstance(self.host, IWorkfileHost): - self.host.save_current_workfile(filepath) + self.host.save_workfile(filepath) else: self.host.save_file(filepath) else: From 918ee531838c283cd4ac8312215ae7351ba8fadf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 14:10:35 +0200 Subject: [PATCH 21/22] renamed 'get_referenced_containers' -> 'get_containers' --- openpype/host/host.py | 8 ++++---- openpype/hosts/maya/api/pipeline.py | 2 +- openpype/tools/sceneinventory/model.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index b7e31d0854..48907e7ec7 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -234,14 +234,14 @@ class ILoadHost: raise MissingMethodsError(host, missing) @abstractmethod - def get_referenced_containers(self): + def get_containers(self): """Retreive referenced containers from scene. This can be implemented in hosts where referencing can be used. Todo: Rename function to something more self explanatory. - Suggestion: 'get_referenced_containers' + Suggestion: 'get_containers' Returns: list[dict]: Information about loaded containers. @@ -251,13 +251,13 @@ class ILoadHost: # --- Deprecated method names --- def ls(self): - """Deprecated variant of 'get_referenced_containers'. + """Deprecated variant of 'get_containers'. Todo: Remove when all usages are replaced. """ - return self.get_referenced_containers() + return self.get_containers() @six.add_metaclass(ABCMeta) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index bad77f00c9..d08e8d1926 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -115,7 +115,7 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost): def get_workfile_extensions(self): return file_extensions() - def get_referenced_containers(self): + def get_containers(self): return ls() @contextlib.contextmanager diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 2894932e96..63fbe04c5c 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -195,7 +195,7 @@ class InventoryModel(TreeModel): host = registered_host() if not items: # for debugging or testing, injecting items from outside if isinstance(host, ILoadHost): - items = host.get_referenced_containers() + items = host.get_containers() else: items = host.ls() From f846ef45d473ba4849f4bb0ed73fc7f9fcb1a546 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 10:52:12 +0200 Subject: [PATCH 22/22] removed signature validation from host registration --- openpype/pipeline/context_tools.py | 65 ------------------------------ 1 file changed, 65 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 4a147c230b..f1b7b565c3 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -1,11 +1,9 @@ """Core pipeline functionality""" import os -import sys import json import types import logging -import inspect import platform import pyblish.api @@ -235,73 +233,10 @@ def register_host(host): required, or browse the source code. """ - signatures = { - "ls": [] - } - _validate_signature(host, signatures) _registered_host["_"] = host -def _validate_signature(module, signatures): - # Required signatures for each member - - missing = list() - invalid = list() - success = True - - for member in signatures: - if not hasattr(module, member): - missing.append(member) - success = False - - else: - attr = getattr(module, member) - if sys.version_info.major >= 3: - signature = inspect.getfullargspec(attr)[0] - else: - signature = inspect.getargspec(attr)[0] - required_signature = signatures[member] - - assert isinstance(signature, list) - assert isinstance(required_signature, list) - - if not all(member in signature - for member in required_signature): - invalid.append({ - "member": member, - "signature": ", ".join(signature), - "required": ", ".join(required_signature) - }) - success = False - - if not success: - report = list() - - if missing: - report.append( - "Incomplete interface for module: '%s'\n" - "Missing: %s" % (module, ", ".join( - "'%s'" % member for member in missing)) - ) - - if invalid: - report.append( - "'%s': One or more members were found, but didn't " - "have the right argument signature." % module.__name__ - ) - - for member in invalid: - report.append( - " Found: {member}({signature})".format(**member) - ) - report.append( - " Expected: {member}({required})".format(**member) - ) - - raise ValueError("\n".join(report)) - - def registered_host(): """Return currently registered host""" return _registered_host["_"]