From 83de346d990ac9e86c10cf85548483d5db17e176 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 16 May 2022 14:59:37 +0200 Subject: [PATCH 001/129] Implement update, remove, switch + fix vdb sequence support --- .../maya/plugins/load/load_vdb_to_redshift.py | 87 +++++++++++++++++-- 1 file changed, 81 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py index 70bd9d22e2..7867f49bd1 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -1,11 +1,21 @@ import os from openpype.api import get_project_settings -from openpype.pipeline import load +from openpype.pipeline import ( + load, + get_representation_path +) class LoadVDBtoRedShift(load.LoaderPlugin): - """Load OpenVDB in a Redshift Volume Shape""" + """Load OpenVDB in a Redshift Volume Shape + + Note that the RedshiftVolumeShape is created without a RedshiftVolume + shader assigned. To get the Redshift volume to render correctly assign + a RedshiftVolume shader (in the Hypershade) and set the density, scatter + and emission channels to the channel names of the volumes in the VDB file. + + """ families = ["vdbcache"] representations = ["vdb"] @@ -55,7 +65,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin): # Root group label = "{}:{}".format(namespace, name) - root = cmds.group(name=label, empty=True) + root = cmds.createNode("transform", name=label) settings = get_project_settings(os.environ['AVALON_PROJECT']) colors = settings['maya']['load']['colors'] @@ -74,9 +84,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin): name="{}RVSShape".format(label), parent=root) - cmds.setAttr("{}.fileName".format(volume_node), - self.fname, - type="string") + self._apply_settings(volume_node, path=self.fname) nodes = [root, volume_node] self[:] = nodes @@ -87,3 +95,70 @@ class LoadVDBtoRedShift(load.LoaderPlugin): nodes=nodes, context=context, loader=self.__class__.__name__) + + def _apply_settings(self, + grid_node, + path): + """Apply the settings for the VDB path to the VRayVolumeGrid""" + from maya import cmds + + # The path points to a single file. However the vdb files could be + # either just that single file or a sequence in a folder so we check + # whether it's a sequence + folder = os.path.dirname(path) + files = os.listdir(folder) + is_single_file = len(files) == 1 + if is_single_file: + filename = path + else: + # The path points to the publish .vdb sequence filepath so we + # find the first file in there that ends with .vdb + files = sorted(files) + first = next((x for x in files if x.endswith(".vdb")), None) + if first is None: + raise RuntimeError("Couldn't find first .vdb file of " + "sequence in: %s" % path) + filename = os.path.join(path, first) + + # Tell Redshift whether it should load as sequence or single file + cmds.setAttr(grid_node + ".useFrameExtension", not is_single_file) + + # Set file path + cmds.setAttr(grid_node + ".fileName", filename, type="string") + + def update(self, container, representation): + from maya import cmds + + path = get_representation_path(representation) + + # Find VRayVolumeGrid + members = cmds.sets(container['objectName'], query=True) + grid_nodes = cmds.ls(members, type="RedshiftVolumeShape", long=True) + assert len(grid_nodes) == 1, "This is a bug" + + # Update the VRayVolumeGrid + self._apply_settings(grid_nodes[0], path=path) + + # Update container representation + cmds.setAttr(container["objectName"] + ".representation", + str(representation["_id"]), + type="string") + + def remove(self, container): + from maya import cmds + + # Get all members of the avalon container, ensure they are unlocked + # and delete everything + members = cmds.sets(container['objectName'], query=True) + cmds.lockNode(members, lock=False) + cmds.delete([container['objectName']] + members) + + # Clean up the namespace + try: + cmds.namespace(removeNamespace=container['namespace'], + deleteNamespaceContent=True) + except RuntimeError: + pass + + def switch(self, container, representation): + self.update(container, representation) From d9bde0c3c47012ae3a8ddf090822089344109708 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Jun 2022 18:38:43 +0200 Subject: [PATCH 002/129] 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 003/129] 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 004/129] 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 005/129] 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 006/129] 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 007/129] 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 008/129] 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 009/129] 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 010/129] 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 09e92ebad87c09d0186f759ac738fc2389270ec3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 Jun 2022 15:20:05 +0200 Subject: [PATCH 011/129] flame: make sure `representations` key is always on instance data --- .../hosts/flame/plugins/publish/collect_timeline_instances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 0aca7c38d5..aa19b78bf1 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -125,7 +125,8 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): "flameAddTasks": self.add_tasks, "tasks": { task["name"]: {"type": task["type"]} - for task in self.add_tasks} + for task in self.add_tasks}, + "representations": [] }) self.log.debug("__ inst_data: {}".format(pformat(inst_data))) From be328e5396760f683c42ed21ca01385e42ec2cf0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 Jun 2022 15:20:34 +0200 Subject: [PATCH 012/129] flame: implementing `keep_original_representation` switch --- .../hosts/flame/plugins/publish/extract_subset_resources.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 0bad3f7cfc..255d57a8ee 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -22,6 +22,8 @@ class ExtractSubsetResources(openpype.api.Extractor): hosts = ["flame"] # plugin defaults + keep_original_representation = False + default_presets = { "thumbnail": { "active": True, @@ -44,7 +46,9 @@ class ExtractSubsetResources(openpype.api.Extractor): export_presets_mapping = {} def process(self, instance): - if "representations" not in instance.data: + + if not self.keep_original_representation: + # remove previeous representation if not needed instance.data["representations"] = [] # flame objects From 84134acb7c4291d1bf864e34f68a5a47f3da513c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Jun 2022 15:35:03 +0200 Subject: [PATCH 013/129] copied editorial to openpype.pipeline --- openpype/pipeline/editorial.py | 282 +++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 openpype/pipeline/editorial.py diff --git a/openpype/pipeline/editorial.py b/openpype/pipeline/editorial.py new file mode 100644 index 0000000000..f62a1842e0 --- /dev/null +++ b/openpype/pipeline/editorial.py @@ -0,0 +1,282 @@ +import os +import re +import clique + +import opentimelineio as otio +from opentimelineio import opentime as _ot + + +def otio_range_to_frame_range(otio_range): + start = _ot.to_frames( + otio_range.start_time, otio_range.start_time.rate) + end = start + _ot.to_frames( + otio_range.duration, otio_range.duration.rate) + return start, end + + +def otio_range_with_handles(otio_range, instance): + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + handles_duration = handle_start + handle_end + fps = float(otio_range.start_time.rate) + start = _ot.to_frames(otio_range.start_time, fps) + duration = _ot.to_frames(otio_range.duration, fps) + + return _ot.TimeRange( + start_time=_ot.RationalTime((start - handle_start), fps), + duration=_ot.RationalTime((duration + handles_duration), fps) + ) + + +def is_overlapping_otio_ranges(test_otio_range, main_otio_range, strict=False): + test_start, test_end = otio_range_to_frame_range(test_otio_range) + main_start, main_end = otio_range_to_frame_range(main_otio_range) + covering_exp = bool( + (test_start <= main_start) and (test_end >= main_end) + ) + inside_exp = bool( + (test_start >= main_start) and (test_end <= main_end) + ) + overlaying_right_exp = bool( + (test_start <= main_end) and (test_end >= main_end) + ) + overlaying_left_exp = bool( + (test_end >= main_start) and (test_start <= main_start) + ) + + if not strict: + return any(( + covering_exp, + inside_exp, + overlaying_right_exp, + overlaying_left_exp + )) + else: + return covering_exp + + +def convert_to_padded_path(path, padding): + """ + Return correct padding in sequence string + + Args: + path (str): path url or simple file name + padding (int): number of padding + + Returns: + type: string with reformated path + + Example: + convert_to_padded_path("plate.%d.exr") > plate.%04d.exr + + """ + if "%d" in path: + path = re.sub("%d", "%0{padding}d".format(padding=padding), path) + return path + + +def trim_media_range(media_range, source_range): + """ + Trim input media range with clip source range. + + Args: + media_range (otio._ot._ot.TimeRange): available range of media + source_range (otio._ot._ot.TimeRange): clip required range + + Returns: + otio._ot._ot.TimeRange: trimmed media range + + """ + rw_media_start = _ot.RationalTime( + media_range.start_time.value + source_range.start_time.value, + media_range.start_time.rate + ) + rw_media_duration = _ot.RationalTime( + source_range.duration.value, + media_range.duration.rate + ) + return _ot.TimeRange( + rw_media_start, rw_media_duration) + + +def range_from_frames(start, duration, fps): + """ + Returns otio time range. + + Args: + start (int): frame start + duration (int): frame duration + fps (float): frame range + + Returns: + otio._ot._ot.TimeRange: created range + + """ + return _ot.TimeRange( + _ot.RationalTime(start, fps), + _ot.RationalTime(duration, fps) + ) + + +def frames_to_seconds(frames, framerate): + """ + Returning seconds. + + Args: + frames (int): frame + framerate (float): frame rate + + Returns: + float: second value + """ + + rt = _ot.from_frames(frames, framerate) + return _ot.to_seconds(rt) + + +def frames_to_timecode(frames, framerate): + rt = _ot.from_frames(frames, framerate) + return _ot.to_timecode(rt) + + +def make_sequence_collection(path, otio_range, metadata): + """ + Make collection from path otio range and otio metadata. + + Args: + path (str): path to image sequence with `%d` + otio_range (otio._ot._ot.TimeRange): range to be used + metadata (dict): data where padding value can be found + + Returns: + list: dir_path (str): path to sequence, collection object + + """ + if "%" not in path: + return None + file_name = os.path.basename(path) + dir_path = os.path.dirname(path) + head = file_name.split("%")[0] + tail = os.path.splitext(file_name)[-1] + first, last = otio_range_to_frame_range(otio_range) + collection = clique.Collection( + head=head, tail=tail, padding=metadata["padding"]) + collection.indexes.update([i for i in range(first, last)]) + return dir_path, collection + + +def _sequence_resize(source, length): + step = float(len(source) - 1) / (length - 1) + for i in range(length): + low, ratio = divmod(i * step, 1) + high = low + 1 if ratio > 0 else low + yield (1 - ratio) * source[int(low)] + ratio * source[int(high)] + + +def get_media_range_with_retimes(otio_clip, handle_start, handle_end): + source_range = otio_clip.source_range + available_range = otio_clip.available_range() + media_in = available_range.start_time.value + media_out = available_range.end_time_inclusive().value + + # modifiers + time_scalar = 1. + offset_in = 0 + offset_out = 0 + time_warp_nodes = [] + + # Check for speed effects and adjust playback speed accordingly + for effect in otio_clip.effects: + if isinstance(effect, otio.schema.LinearTimeWarp): + time_scalar = effect.time_scalar + + elif isinstance(effect, otio.schema.FreezeFrame): + # For freeze frame, playback speed must be set after range + time_scalar = 0. + + elif isinstance(effect, otio.schema.TimeEffect): + # For freeze frame, playback speed must be set after range + name = effect.name + effect_name = effect.effect_name + if "TimeWarp" not in effect_name: + continue + metadata = effect.metadata + lookup = metadata.get("lookup") + if not lookup: + continue + + # time warp node + tw_node = { + "Class": "TimeWarp", + "name": name + } + tw_node.update(metadata) + tw_node["lookup"] = list(lookup) + + # get first and last frame offsets + offset_in += lookup[0] + offset_out += lookup[-1] + + # add to timewarp nodes + time_warp_nodes.append(tw_node) + + # multiply by time scalar + offset_in *= time_scalar + offset_out *= time_scalar + + # filip offset if reversed speed + if time_scalar < 0: + _offset_in = offset_out + _offset_out = offset_in + offset_in = _offset_in + offset_out = _offset_out + + # scale handles + handle_start *= abs(time_scalar) + handle_end *= abs(time_scalar) + + # filip handles if reversed speed + if time_scalar < 0: + _handle_start = handle_end + _handle_end = handle_start + handle_start = _handle_start + handle_end = _handle_end + + source_in = source_range.start_time.value + + media_in_trimmed = ( + media_in + source_in + offset_in) + media_out_trimmed = ( + media_in + source_in + ( + ((source_range.duration.value - 1) * abs( + time_scalar)) + offset_out)) + + # calculate available handles + if (media_in_trimmed - media_in) < handle_start: + handle_start = (media_in_trimmed - media_in) + if (media_out - media_out_trimmed) < handle_end: + handle_end = (media_out - media_out_trimmed) + + # create version data + version_data = { + "versionData": { + "retime": True, + "speed": time_scalar, + "timewarps": time_warp_nodes, + "handleStart": round(handle_start), + "handleEnd": round(handle_end) + } + } + + returning_dict = { + "mediaIn": media_in_trimmed, + "mediaOut": media_out_trimmed, + "handleStart": round(handle_start), + "handleEnd": round(handle_end) + } + + # add version data only if retime + if time_warp_nodes or time_scalar != 1.: + returning_dict.update(version_data) + + return returning_dict From 228c84af83d111918b5366be2625d1a869584058 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Jun 2022 15:36:17 +0200 Subject: [PATCH 014/129] import editorial functions from openpype.pipeline.editorial --- .../publish/collect_timeline_instances.py | 9 +++++--- .../plugins/publish/precollect_instances.py | 6 ++--- openpype/hosts/resolve/api/lib.py | 4 ++-- .../publish/collect_otio_frame_ranges.py | 11 ++++++---- .../publish/collect_otio_subset_resources.py | 12 ++++++---- .../plugins/publish/extract_otio_review.py | 22 ++++++++++++------- .../publish/extract_otio_trimming_video.py | 5 +++-- 7 files changed, 43 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index 0aca7c38d5..8c2d172732 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -2,7 +2,10 @@ import re import pyblish import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export -import openpype.lib as oplib +from openpype.pipeline.editorial import ( + is_overlapping_otio_ranges, + get_media_range_with_retimes +) # # developer reload modules from pprint import pformat @@ -271,7 +274,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): # HACK: it is here to serve for versions bellow 2021.1 if not any([head, tail]): - retimed_attributes = oplib.get_media_range_with_retimes( + retimed_attributes = get_media_range_with_retimes( otio_clip, handle_start, handle_end) self.log.debug( ">> retimed_attributes: {}".format(retimed_attributes)) @@ -370,7 +373,7 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): continue if otio_clip.name not in segment.name.get_value(): continue - if oplib.is_overlapping_otio_ranges( + if is_overlapping_otio_ranges( parent_range, timeline_range, strict=True): # add pypedata marker to otio_clip metadata diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index b891a37d9d..2d0ec6fc99 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -1,5 +1,5 @@ import pyblish -import openpype +from openpype.pipeline.editorial import is_overlapping_otio_ranges from openpype.hosts.hiero import api as phiero from openpype.hosts.hiero.api.otio import hiero_export import hiero @@ -275,7 +275,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): parent_range = otio_audio.range_in_parent() # if any overaling clip found then return True - if openpype.lib.is_overlapping_otio_ranges( + if is_overlapping_otio_ranges( parent_range, timeline_range, strict=False): return True @@ -304,7 +304,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): continue self.log.debug("__ parent_range: {}".format(parent_range)) self.log.debug("__ timeline_range: {}".format(timeline_range)) - if openpype.lib.is_overlapping_otio_ranges( + if is_overlapping_otio_ranges( parent_range, timeline_range, strict=True): # add pypedata marker to otio_clip metadata diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 22f83c6eed..c4717bd370 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -4,7 +4,7 @@ import re import os import contextlib from opentimelineio import opentime -import openpype +from openpype.pipeline.editorial import is_overlapping_otio_ranges from ..otio import davinci_export as otio_export @@ -824,7 +824,7 @@ def get_otio_clip_instance_data(otio_timeline, timeline_item_data): continue if otio_clip.name not in timeline_item.GetName(): continue - if openpype.lib.is_overlapping_otio_ranges( + if is_overlapping_otio_ranges( parent_range, timeline_range, strict=True): # add pypedata marker to otio_clip metadata diff --git a/openpype/plugins/publish/collect_otio_frame_ranges.py b/openpype/plugins/publish/collect_otio_frame_ranges.py index 8eaf9d6f29..c86e777850 100644 --- a/openpype/plugins/publish/collect_otio_frame_ranges.py +++ b/openpype/plugins/publish/collect_otio_frame_ranges.py @@ -8,8 +8,11 @@ Requires: # import os import opentimelineio as otio import pyblish.api -import openpype.lib from pprint import pformat +from openpype.pipeline.editorial import ( + otio_range_to_frame_range, + otio_range_with_handles +) class CollectOtioFrameRanges(pyblish.api.InstancePlugin): @@ -31,9 +34,9 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): otio_tl_range = otio_clip.range_in_parent() otio_src_range = otio_clip.source_range otio_avalable_range = otio_clip.available_range() - otio_tl_range_handles = openpype.lib.otio_range_with_handles( + otio_tl_range_handles = otio_range_with_handles( otio_tl_range, instance) - otio_src_range_handles = openpype.lib.otio_range_with_handles( + otio_src_range_handles = otio_range_with_handles( otio_src_range, instance) # get source avalable start frame @@ -42,7 +45,7 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): otio_avalable_range.start_time.rate) # convert to frames - range_convert = openpype.lib.otio_range_to_frame_range + range_convert = otio_range_to_frame_range tl_start, tl_end = range_convert(otio_tl_range) tl_start_h, tl_end_h = range_convert(otio_tl_range_handles) src_start, src_end = range_convert(otio_src_range) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 40d4f35bdc..fc6a9b50f2 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -10,7 +10,11 @@ import os import clique import opentimelineio as otio import pyblish.api -import openpype.lib as oplib +from openpype.pipeline.editorial import ( + get_media_range_with_retimes, + range_from_frames, + make_sequence_collection +) class CollectOtioSubsetResources(pyblish.api.InstancePlugin): @@ -42,7 +46,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): available_duration = otio_avalable_range.duration.value # get available range trimmed with processed retimes - retimed_attributes = oplib.get_media_range_with_retimes( + retimed_attributes = get_media_range_with_retimes( otio_clip, handle_start, handle_end) self.log.debug( ">> retimed_attributes: {}".format(retimed_attributes)) @@ -64,7 +68,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): a_frame_end_h = media_out + handle_end # create trimmed otio time range - trimmed_media_range_h = oplib.range_from_frames( + trimmed_media_range_h = range_from_frames( a_frame_start_h, (a_frame_end_h - a_frame_start_h) + 1, media_fps ) @@ -144,7 +148,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): # in case it is file sequence but not new OTIO schema # `ImageSequenceReference` path = media_ref.target_url - collection_data = oplib.make_sequence_collection( + collection_data = make_sequence_collection( path, trimmed_media_range_h, metadata) self.staging_dir, collection = collection_data diff --git a/openpype/plugins/publish/extract_otio_review.py b/openpype/plugins/publish/extract_otio_review.py index 35adc97442..2ce5323468 100644 --- a/openpype/plugins/publish/extract_otio_review.py +++ b/openpype/plugins/publish/extract_otio_review.py @@ -19,6 +19,13 @@ import clique import opentimelineio as otio from pyblish import api import openpype +from openpype.pipeline.editorial import ( + otio_range_to_frame_range, + trim_media_range, + range_from_frames, + frames_to_seconds, + make_sequence_collection +) class ExtractOTIOReview(openpype.api.Extractor): @@ -161,7 +168,7 @@ class ExtractOTIOReview(openpype.api.Extractor): dirname = media_ref.target_url_base head = media_ref.name_prefix tail = media_ref.name_suffix - first, last = openpype.lib.otio_range_to_frame_range( + first, last = otio_range_to_frame_range( available_range) collection = clique.Collection( head=head, @@ -180,7 +187,7 @@ class ExtractOTIOReview(openpype.api.Extractor): # in case it is file sequence but not new OTIO schema # `ImageSequenceReference` path = media_ref.target_url - collection_data = openpype.lib.make_sequence_collection( + collection_data = make_sequence_collection( path, available_range, metadata) dir_path, collection = collection_data @@ -305,8 +312,8 @@ class ExtractOTIOReview(openpype.api.Extractor): duration = avl_durtation # return correct trimmed range - return openpype.lib.trim_media_range( - avl_range, openpype.lib.range_from_frames(start, duration, fps) + return trim_media_range( + avl_range, range_from_frames(start, duration, fps) ) def _render_seqment(self, sequence=None, @@ -357,8 +364,8 @@ class ExtractOTIOReview(openpype.api.Extractor): frame_start = otio_range.start_time.value input_fps = otio_range.start_time.rate frame_duration = otio_range.duration.value - sec_start = openpype.lib.frames_to_secons(frame_start, input_fps) - sec_duration = openpype.lib.frames_to_secons( + sec_start = frames_to_seconds(frame_start, input_fps) + sec_duration = frames_to_seconds( frame_duration, input_fps ) @@ -370,8 +377,7 @@ class ExtractOTIOReview(openpype.api.Extractor): ]) elif gap: - sec_duration = openpype.lib.frames_to_secons( - gap, self.actual_fps) + sec_duration = frames_to_seconds(gap, self.actual_fps) # form command for rendering gap files command.extend([ diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index e8e2994f36..19625fa568 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -9,6 +9,7 @@ import os from pyblish import api import openpype from copy import deepcopy +from openpype.pipeline.editorial import frames_to_seconds class ExtractOTIOTrimmingVideo(openpype.api.Extractor): @@ -81,8 +82,8 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): frame_start = otio_range.start_time.value input_fps = otio_range.start_time.rate frame_duration = otio_range.duration.value - 1 - sec_start = openpype.lib.frames_to_secons(frame_start, input_fps) - sec_duration = openpype.lib.frames_to_secons(frame_duration, input_fps) + sec_start = frames_to_seconds(frame_start, input_fps) + sec_duration = frames_to_seconds(frame_duration, input_fps) # form command for rendering gap files command.extend([ From c4f3ad901b8bea13735bf3a2308d7c3ee957d8a2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Jun 2022 15:36:57 +0200 Subject: [PATCH 015/129] kept editorial functions in openpype.lib with deprecated decorator --- openpype/lib/editorial.py | 315 +++++++------------------------------- 1 file changed, 59 insertions(+), 256 deletions(-) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 32d6dc3688..2730ba1f3c 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -1,289 +1,92 @@ -import os -import re -import clique -from .import_utils import discover_host_vendor_module - -try: - import opentimelineio as otio - from opentimelineio import opentime as _ot -except ImportError: - if not os.environ.get("AVALON_APP"): - raise - otio = discover_host_vendor_module("opentimelineio") - _ot = discover_host_vendor_module("opentimelineio.opentime") +import warnings +import functools -def otio_range_to_frame_range(otio_range): - start = _ot.to_frames( - otio_range.start_time, otio_range.start_time.rate) - end = start + _ot.to_frames( - otio_range.duration, otio_range.duration.rate) - return start, end +def editorial_deprecated(func): + """Mark functions as deprecated. - -def otio_range_with_handles(otio_range, instance): - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] - handles_duration = handle_start + handle_end - fps = float(otio_range.start_time.rate) - start = _ot.to_frames(otio_range.start_time, fps) - duration = _ot.to_frames(otio_range.duration, fps) - - return _ot.TimeRange( - start_time=_ot.RationalTime((start - handle_start), fps), - duration=_ot.RationalTime((duration + handles_duration), fps) - ) - - -def is_overlapping_otio_ranges(test_otio_range, main_otio_range, strict=False): - test_start, test_end = otio_range_to_frame_range(test_otio_range) - main_start, main_end = otio_range_to_frame_range(main_otio_range) - covering_exp = bool( - (test_start <= main_start) and (test_end >= main_end) - ) - inside_exp = bool( - (test_start >= main_start) and (test_end <= main_end) - ) - overlaying_right_exp = bool( - (test_start <= main_end) and (test_end >= main_end) - ) - overlaying_left_exp = bool( - (test_end >= main_start) and (test_start <= main_start) - ) - - if not strict: - return any(( - covering_exp, - inside_exp, - overlaying_right_exp, - overlaying_left_exp - )) - else: - return covering_exp - - -def convert_to_padded_path(path, padding): + It will result in a warning being emitted when the function is used. """ - Return correct padding in sequence string - Args: - path (str): path url or simple file name - padding (int): number of padding - - Returns: - type: string with reformated path - - Example: - convert_to_padded_path("plate.%d.exr") > plate.%04d.exr - - """ - if "%d" in path: - path = re.sub("%d", "%0{padding}d".format(padding=padding), path) - return path + @functools.wraps(func) + def new_func(*args, **kwargs): + warnings.warn( + ( + "Call to deprecated function '{}'." + " Function was moved to 'openpype.pipeline.editorial'." + ).format(func.__name__), + category=DeprecationWarning, + stacklevel=2 + ) + return func(*args, **kwargs) + return new_func -def trim_media_range(media_range, source_range): - """ - Trim input media range with clip source range. +@editorial_deprecated +def otio_range_to_frame_range(*args, **kwargs): + from openpype.pipeline.editorial import otio_range_to_frame_range - Args: - media_range (otio._ot._ot.TimeRange): available range of media - source_range (otio._ot._ot.TimeRange): clip required range - - Returns: - otio._ot._ot.TimeRange: trimmed media range - - """ - rw_media_start = _ot.RationalTime( - media_range.start_time.value + source_range.start_time.value, - media_range.start_time.rate - ) - rw_media_duration = _ot.RationalTime( - source_range.duration.value, - media_range.duration.rate - ) - return _ot.TimeRange( - rw_media_start, rw_media_duration) + return otio_range_to_frame_range(*args, **kwargs) -def range_from_frames(start, duration, fps): - """ - Returns otio time range. +@editorial_deprecated +def otio_range_with_handles(*args, **kwargs): + from openpype.pipeline.editorial import otio_range_with_handles - Args: - start (int): frame start - duration (int): frame duration - fps (float): frame range - - Returns: - otio._ot._ot.TimeRange: created range - - """ - return _ot.TimeRange( - _ot.RationalTime(start, fps), - _ot.RationalTime(duration, fps) - ) + return otio_range_with_handles(*args, **kwargs) -def frames_to_secons(frames, framerate): - """ - Returning secons. +@editorial_deprecated +def is_overlapping_otio_ranges(*args, **kwargs): + from openpype.pipeline.editorial import is_overlapping_otio_ranges - Args: - frames (int): frame - framerate (float): frame rate - - Returns: - float: second value - - """ - rt = _ot.from_frames(frames, framerate) - return _ot.to_seconds(rt) + return is_overlapping_otio_ranges(*args, **kwargs) -def frames_to_timecode(frames, framerate): - rt = _ot.from_frames(frames, framerate) - return _ot.to_timecode(rt) +@editorial_deprecated +def convert_to_padded_path(*args, **kwargs): + from openpype.pipeline.editorial import convert_to_padded_path + + return convert_to_padded_path(*args, **kwargs) -def make_sequence_collection(path, otio_range, metadata): - """ - Make collection from path otio range and otio metadata. +@editorial_deprecated +def trim_media_range(*args, **kwargs): + from openpype.pipeline.editorial import trim_media_range - Args: - path (str): path to image sequence with `%d` - otio_range (otio._ot._ot.TimeRange): range to be used - metadata (dict): data where padding value can be found - - Returns: - list: dir_path (str): path to sequence, collection object - - """ - if "%" not in path: - return None - file_name = os.path.basename(path) - dir_path = os.path.dirname(path) - head = file_name.split("%")[0] - tail = os.path.splitext(file_name)[-1] - first, last = otio_range_to_frame_range(otio_range) - collection = clique.Collection( - head=head, tail=tail, padding=metadata["padding"]) - collection.indexes.update([i for i in range(first, last)]) - return dir_path, collection + return trim_media_range(*args, **kwargs) -def _sequence_resize(source, length): - step = float(len(source) - 1) / (length - 1) - for i in range(length): - low, ratio = divmod(i * step, 1) - high = low + 1 if ratio > 0 else low - yield (1 - ratio) * source[int(low)] + ratio * source[int(high)] +@editorial_deprecated +def range_from_frames(*args, **kwargs): + from openpype.pipeline.editorial import range_from_frames + + return range_from_frames(*args, **kwargs) -def get_media_range_with_retimes(otio_clip, handle_start, handle_end): - source_range = otio_clip.source_range - available_range = otio_clip.available_range() - media_in = available_range.start_time.value - media_out = available_range.end_time_inclusive().value +@editorial_deprecated +def frames_to_seconds(*args, **kwargs): + from openpype.pipeline.editorial import frames_to_seconds - # modifiers - time_scalar = 1. - offset_in = 0 - offset_out = 0 - time_warp_nodes = [] + return frames_to_seconds(*args, **kwargs) - # Check for speed effects and adjust playback speed accordingly - for effect in otio_clip.effects: - if isinstance(effect, otio.schema.LinearTimeWarp): - time_scalar = effect.time_scalar - elif isinstance(effect, otio.schema.FreezeFrame): - # For freeze frame, playback speed must be set after range - time_scalar = 0. +@editorial_deprecated +def frames_to_timecode(*args, **kwargs): + from openpype.pipeline.editorial import frames_to_timecode - elif isinstance(effect, otio.schema.TimeEffect): - # For freeze frame, playback speed must be set after range - name = effect.name - effect_name = effect.effect_name - if "TimeWarp" not in effect_name: - continue - metadata = effect.metadata - lookup = metadata.get("lookup") - if not lookup: - continue + return frames_to_timecode(*args, **kwargs) - # time warp node - tw_node = { - "Class": "TimeWarp", - "name": name - } - tw_node.update(metadata) - tw_node["lookup"] = list(lookup) - # get first and last frame offsets - offset_in += lookup[0] - offset_out += lookup[-1] +@editorial_deprecated +def make_sequence_collection(*args, **kwargs): + from openpype.pipeline.editorial import make_sequence_collection - # add to timewarp nodes - time_warp_nodes.append(tw_node) + return make_sequence_collection(*args, **kwargs) - # multiply by time scalar - offset_in *= time_scalar - offset_out *= time_scalar - # filip offset if reversed speed - if time_scalar < 0: - _offset_in = offset_out - _offset_out = offset_in - offset_in = _offset_in - offset_out = _offset_out +@editorial_deprecated +def get_media_range_with_retimes(*args, **kwargs): + from openpype.pipeline.editorial import get_media_range_with_retimes - # scale handles - handle_start *= abs(time_scalar) - handle_end *= abs(time_scalar) - - # filip handles if reversed speed - if time_scalar < 0: - _handle_start = handle_end - _handle_end = handle_start - handle_start = _handle_start - handle_end = _handle_end - - source_in = source_range.start_time.value - - media_in_trimmed = ( - media_in + source_in + offset_in) - media_out_trimmed = ( - media_in + source_in + ( - ((source_range.duration.value - 1) * abs( - time_scalar)) + offset_out)) - - # calculate available handles - if (media_in_trimmed - media_in) < handle_start: - handle_start = (media_in_trimmed - media_in) - if (media_out - media_out_trimmed) < handle_end: - handle_end = (media_out - media_out_trimmed) - - # create version data - version_data = { - "versionData": { - "retime": True, - "speed": time_scalar, - "timewarps": time_warp_nodes, - "handleStart": round(handle_start), - "handleEnd": round(handle_end) - } - } - - returning_dict = { - "mediaIn": media_in_trimmed, - "mediaOut": media_out_trimmed, - "handleStart": round(handle_start), - "handleEnd": round(handle_end) - } - - # add version data only if retime - if time_warp_nodes or time_scalar != 1.: - returning_dict.update(version_data) - - return returning_dict + return get_media_range_with_retimes(*args, **kwargs) From 4c046b442aef143a065b84c7c933fba14d1a34f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Jun 2022 15:37:14 +0200 Subject: [PATCH 016/129] removed unused discover_host_vendor_module --- openpype/lib/import_utils.py | 25 ------------------------- 1 file changed, 25 deletions(-) delete mode 100644 openpype/lib/import_utils.py diff --git a/openpype/lib/import_utils.py b/openpype/lib/import_utils.py deleted file mode 100644 index e88c07fca6..0000000000 --- a/openpype/lib/import_utils.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -import sys -import importlib -from .log import PypeLogger as Logger - -log = Logger().get_logger(__name__) - - -def discover_host_vendor_module(module_name): - host = os.environ["AVALON_APP"] - pype_root = os.environ["OPENPYPE_REPOS_ROOT"] - main_module = module_name.split(".")[0] - module_path = os.path.join( - pype_root, "hosts", host, "vendor", main_module) - - log.debug( - "Importing module from host vendor path: `{}`".format(module_path)) - - if not os.path.exists(module_path): - log.warning( - "Path not existing: `{}`".format(module_path)) - return None - - sys.path.insert(1, module_path) - return importlib.import_module(module_name) From d4df16bb87aac5b667bed8b5ecdd9f6afd52f20b Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 17 Jun 2022 08:08:05 +0300 Subject: [PATCH 017/129] Test resolution values against settings --- .../maya/plugins/publish/extract_playblast.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index bb1ecf279d..ce1bb8ae83 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -51,7 +51,21 @@ class ExtractPlayblast(openpype.api.Extractor): ) preset = lib.load_capture_preset(data=self.capture_preset) + capture_presets = openpype.api.get_current_project_settings()["maya"]["publish"]["ExtractPlayblast"]["capture_preset"] # noqa + width_preset = capture_presets["Resolution"]["width"] + height_preset = capture_presets["Resolution"]["height"] preset['camera'] = camera + + if width_preset == 0: + preset['width'] = instance.data.get("resolutionWidth") + else: + preset["width"] = width_preset + + if height_preset == 0: + preset['width'] = instance.data.get("resolutionHeight") + else: + preset['height'] = height_preset + preset['start_frame'] = start preset['end_frame'] = end camera_option = preset.get("camera_option", {}) From 0dfe3b69745fc9d9fb94143aa0cc3039640ab669 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Fri, 17 Jun 2022 15:04:46 +0300 Subject: [PATCH 018/129] Update openpype/hosts/maya/plugins/publish/extract_playblast.py Simplify settings capture statement. Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index ce1bb8ae83..9e1970780b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -51,7 +51,7 @@ class ExtractPlayblast(openpype.api.Extractor): ) preset = lib.load_capture_preset(data=self.capture_preset) - capture_presets = openpype.api.get_current_project_settings()["maya"]["publish"]["ExtractPlayblast"]["capture_preset"] # noqa + capture_presets = self.capture_preset width_preset = capture_presets["Resolution"]["width"] height_preset = capture_presets["Resolution"]["height"] preset['camera'] = camera From d1b0c37c93c5a8c8dfd105d6cae8bc61e074be15 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 17 Jun 2022 18:48:08 +0300 Subject: [PATCH 019/129] Add resolution overrides to creator. --- openpype/hosts/maya/plugins/create/create_review.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index fbf3399f61..331f0818eb 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -15,6 +15,8 @@ class CreateReview(plugin.Creator): keepImages = False isolate = False imagePlane = True + resolutionWidth = 0 + resolutionHeight = 0 transparency = [ "preset", "simple", @@ -33,6 +35,8 @@ class CreateReview(plugin.Creator): for key, value in animation_data.items(): data[key] = value + data["resolutionWidth"] = self.resolutionWidth + data["resolutionHeight"] = self.resolutionHeight data["isolate"] = self.isolate data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane From eb3afe63c53df0969b2717a353f8320c9d69aa0a Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 17 Jun 2022 18:48:23 +0300 Subject: [PATCH 020/129] Add resolution overrides to collector. --- openpype/hosts/maya/plugins/publish/collect_review.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index e9e0d74c03..ded549a849 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -70,6 +70,8 @@ class CollectReview(pyblish.api.InstancePlugin): data['handles'] = instance.data.get('handles', None) data['step'] = instance.data['step'] data['fps'] = instance.data['fps'] + data['resolutionWidth'] = instance.data['resolutionWidth'] + data['resolutionHeight'] = instance.data['resolutionHeight'] data["isolate"] = instance.data["isolate"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) From 491e356bc654352f11feb4609f97071e4a651645 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 17 Jun 2022 18:48:39 +0300 Subject: [PATCH 021/129] Correct resolution overrides in extractor --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 9e1970780b..9e6363502e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -54,15 +54,17 @@ class ExtractPlayblast(openpype.api.Extractor): capture_presets = self.capture_preset width_preset = capture_presets["Resolution"]["width"] height_preset = capture_presets["Resolution"]["height"] + instance_width = instance.data.get("resolutionWidth") + instance_height = instance.data.get("resolutionHeight") preset['camera'] = camera - if width_preset == 0: + if instance_width != 0: preset['width'] = instance.data.get("resolutionWidth") else: preset["width"] = width_preset - if height_preset == 0: - preset['width'] = instance.data.get("resolutionHeight") + if instance_height != 0: + preset['height'] = instance.data.get("resolutionHeight") else: preset['height'] = height_preset From 594707f8340ea3b9cbc9ac4df6193a1e3dc95a76 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Sat, 18 Jun 2022 00:34:25 +0300 Subject: [PATCH 022/129] Add comments --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 9e6363502e..32cbeed81b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -50,14 +50,20 @@ class ExtractPlayblast(openpype.api.Extractor): ['override_viewport_options'] ) preset = lib.load_capture_preset(data=self.capture_preset) - + # Grab capture presets from the project settings capture_presets = self.capture_preset + # Set resolution variables from capture presets width_preset = capture_presets["Resolution"]["width"] height_preset = capture_presets["Resolution"]["height"] + # Set resolution variables from instance values instance_width = instance.data.get("resolutionWidth") instance_height = instance.data.get("resolutionHeight") preset['camera'] = camera + # Tests if instance resolution width is set, + # if it is a value other than zero, that value is + # used, if not then the project settings resolution is + # used if instance_width != 0: preset['width'] = instance.data.get("resolutionWidth") else: From c762fa92f6ab7f692c751fefdffe5f9c9571d582 Mon Sep 17 00:00:00 2001 From: macman Date: Mon, 20 Jun 2022 13:39:52 +0300 Subject: [PATCH 023/129] Handle excluding `model` family from frame range validator. --- openpype/hosts/maya/plugins/publish/validate_frame_range.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_frame_range.py b/openpype/hosts/maya/plugins/publish/validate_frame_range.py index 98b5b4d79b..4415815d32 100644 --- a/openpype/hosts/maya/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/maya/plugins/publish/validate_frame_range.py @@ -27,6 +27,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): "yeticache"] optional = True actions = [openpype.api.RepairAction] + exclude_families = ["model"] def process(self, instance): context = instance.context @@ -56,7 +57,9 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): # compare with data on instance errors = [] - + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return if(inst_start != frame_start_handle): errors.append("Instance start frame [ {} ] doesn't " "match the one set on instance [ {} ]: " From 3a1d9c9fcadab29f3b2962527dedea4da49abd4a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Jun 2022 13:11:01 +0200 Subject: [PATCH 024/129] Added far future value for null values for dates Null values were sorted as last, this keeps queued items together with last synched. --- openpype/modules/sync_server/tray/models.py | 60 +++++++++++++-------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index c49edeafb9..6d1e85c17a 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -1,6 +1,7 @@ import os import attr from bson.objectid import ObjectId +import datetime from Qt import QtCore from Qt.QtCore import Qt @@ -413,6 +414,23 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): return index return None + def _convert_date(self, date_value, current_date): + """Converts 'date_value' to string. + + Value of date_value might contain date in the future, used for nicely + sort queued items next to last downloaded. + """ + try: + converted_date = None + # ignore date in the future - for sorting only + if date_value and date_value < current_date: + converted_date = date_value.strftime("%Y%m%dT%H%M%SZ") + except (AttributeError, TypeError): + # ignore unparseable values + pass + + return converted_date + class SyncRepresentationSummaryModel(_SyncRepresentationModel): """ @@ -560,7 +578,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): remote_provider = lib.translate_provider_for_icon(self.sync_server, self.project, remote_site) - + current_date = datetime.datetime.now() for repre in result.get("paginatedResults"): files = repre.get("files", []) if isinstance(files, dict): # aggregate returns dictionary @@ -570,14 +588,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if not files: continue - local_updated = remote_updated = None - if repre.get('updated_dt_local'): - local_updated = \ - repre.get('updated_dt_local').strftime("%Y%m%dT%H%M%SZ") - - if repre.get('updated_dt_remote'): - remote_updated = \ - repre.get('updated_dt_remote').strftime("%Y%m%dT%H%M%SZ") + local_updated = self._convert_date(repre.get('updated_dt_local'), + current_date) + remote_updated = self._convert_date(repre.get('updated_dt_remote'), + current_date) avg_progress_remote = lib.convert_progress( repre.get('avg_progress_remote', '0')) @@ -645,6 +659,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if limit == 0: limit = SyncRepresentationSummaryModel.PAGE_SIZE + # replace null with value in the future for better sorting + dummy_max_date = datetime.datetime(2099, 1, 1) aggr = [ {"$match": self.get_match_part()}, {'$unwind': '$files'}, @@ -687,7 +703,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): {'$cond': [ {'$size': "$order_remote.last_failed_dt"}, "$order_remote.last_failed_dt", - [] + [dummy_max_date] ]} ]}}, 'updated_dt_local': {'$first': { @@ -696,7 +712,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): {'$cond': [ {'$size': "$order_local.last_failed_dt"}, "$order_local.last_failed_dt", - [] + [dummy_max_date] ]} ]}}, 'files_size': {'$ifNull': ["$files.size", 0]}, @@ -1039,6 +1055,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self.project, remote_site) + current_date = datetime.datetime.now() for repre in result.get("paginatedResults"): # log.info("!!! repre:: {}".format(repre)) files = repre.get("files", []) @@ -1046,16 +1063,12 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): files = [files] for file in files: - local_updated = remote_updated = None - if repre.get('updated_dt_local'): - local_updated = \ - repre.get('updated_dt_local').strftime( - "%Y%m%dT%H%M%SZ") - - if repre.get('updated_dt_remote'): - remote_updated = \ - repre.get('updated_dt_remote').strftime( - "%Y%m%dT%H%M%SZ") + local_updated = self._convert_date( + repre.get('updated_dt_local'), + current_date) + remote_updated = self._convert_date( + repre.get('updated_dt_remote'), + current_date) remote_progress = lib.convert_progress( repre.get('progress_remote', '0')) @@ -1104,6 +1117,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if limit == 0: limit = SyncRepresentationSummaryModel.PAGE_SIZE + dummy_max_date = datetime.datetime(2099, 1, 1) aggr = [ {"$match": self.get_match_part()}, {"$unwind": "$files"}, @@ -1147,7 +1161,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): '$cond': [ {'$size': "$order_remote.last_failed_dt"}, "$order_remote.last_failed_dt", - [] + [dummy_max_date] ] } ] @@ -1160,7 +1174,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): '$cond': [ {'$size': "$order_local.last_failed_dt"}, "$order_local.last_failed_dt", - [] + [dummy_max_date] ] } ] From a93b978f354b6ed34034f0f4caaa98b8c637468e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 20 Jun 2022 21:48:08 +0200 Subject: [PATCH 025/129] flame: fixing thumbnail duplication issue --- .../hosts/flame/plugins/publish/extract_subset_resources.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 5e0a5e344d..dd672ec375 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -86,7 +86,11 @@ class ExtractSubsetResources(openpype.api.Extractor): # add default preset type for thumbnail and reviewable video # update them with settings and override in case the same # are found in there - export_presets = deepcopy(self.default_presets) + _preset_keys = [k.split('_')[0] for k in self.export_presets_mapping] + export_presets = { + k: v for k, v in deepcopy(self.default_presets) + if k not in _preset_keys + } export_presets.update(self.export_presets_mapping) # loop all preset names and From 70d9b6fcb73c7ac3abb74d2218fcf2e53e845427 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 20 Jun 2022 22:00:41 +0200 Subject: [PATCH 026/129] flame: fixing dict iter with items --- .../hosts/flame/plugins/publish/extract_subset_resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index dd672ec375..1b6900e405 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -88,7 +88,7 @@ class ExtractSubsetResources(openpype.api.Extractor): # are found in there _preset_keys = [k.split('_')[0] for k in self.export_presets_mapping] export_presets = { - k: v for k, v in deepcopy(self.default_presets) + k: v for k, v in deepcopy(self.default_presets).items() if k not in _preset_keys } export_presets.update(self.export_presets_mapping) From 250f73656ac382e7d9bb441326bbd6f55118e0eb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 20 Jun 2022 22:04:36 +0200 Subject: [PATCH 027/129] Flame: fixing NoneType in abs --- .../flame/plugins/publish/collect_timeline_instances.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py index aa19b78bf1..b8489de758 100644 --- a/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py +++ b/openpype/hosts/flame/plugins/publish/collect_timeline_instances.py @@ -1,4 +1,5 @@ import re +from types import NoneType import pyblish import openpype.hosts.flame.api as opfapi from openpype.hosts.flame.otio import flame_export @@ -75,6 +76,12 @@ class CollectTimelineInstances(pyblish.api.ContextPlugin): marker_data["handleEnd"] ) + # make sure there is not NoneType rather 0 + if isinstance(head, NoneType): + head = 0 + if isinstance(tail, NoneType): + tail = 0 + # make sure value is absolute if head != 0: head = abs(head) From a29a9af927c339acac56711351a3b995846e4111 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 20 Jun 2022 22:11:15 +0200 Subject: [PATCH 028/129] flame: unique name swapped with repre name unique name could be more than `thumbnail` --- .../flame/plugins/publish/extract_subset_resources.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 1b6900e405..3ae8779398 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -228,7 +228,11 @@ class ExtractSubsetResources(openpype.api.Extractor): # make sure only first segment is used if underscore in name # HACK: `ftrackreview_withLUT` will result only in `ftrackreview` - repr_name = unique_name.split("_")[0] + if ( + "thumbnail" in unique_name + or "ftrackreview" in unique_name + ): + repr_name = unique_name.split("_")[0] # create representation data representation_data = { @@ -267,7 +271,7 @@ class ExtractSubsetResources(openpype.api.Extractor): if os.path.splitext(f)[-1] == ".mov" ] # then try if thumbnail is not in unique name - or unique_name == "thumbnail" + or repr_name == "thumbnail" ): representation_data["files"] = files.pop() else: From 035b202b58fc3c8a2e0f381ce4856a4f551e18a4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 20 Jun 2022 22:19:05 +0200 Subject: [PATCH 029/129] Flame: fixing repr_name missing --- openpype/hosts/flame/plugins/publish/extract_subset_resources.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py index 3ae8779398..d34f5d5854 100644 --- a/openpype/hosts/flame/plugins/publish/extract_subset_resources.py +++ b/openpype/hosts/flame/plugins/publish/extract_subset_resources.py @@ -226,6 +226,7 @@ class ExtractSubsetResources(openpype.api.Extractor): opfapi.export_clip( export_dir_path, exporting_clip, preset_path, **export_kwargs) + repr_name = unique_name # make sure only first segment is used if underscore in name # HACK: `ftrackreview_withLUT` will result only in `ftrackreview` if ( From 6de95ba8064a9ec06757dc8d8fc2b0e15e12498b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Jun 2022 11:04:02 +0200 Subject: [PATCH 030/129] resolve is using query functions --- .../hosts/resolve/plugins/load/load_clip.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index cf88b14e81..86dd6850e7 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -1,6 +1,10 @@ from copy import deepcopy from importlib import reload +from openpype.client import ( + get_version_by_id, + get_last_version_by_subset_id, +) from openpype.hosts import resolve from openpype.pipeline import ( get_representation_path, @@ -96,10 +100,8 @@ class LoadClip(resolve.TimelineItemLoader): namespace = container['namespace'] timeline_item_data = resolve.get_pype_timeline_item_by_name(namespace) timeline_item = timeline_item_data["clip"]["item"] - version = legacy_io.find_one({ - "type": "version", - "_id": representation["parent"] - }) + project_name = legacy_io.active_project() + version = get_version_by_id(project_name, representation["parent"]) version_data = version.get("data", {}) version_name = version.get("name", None) colorspace = version_data.get("colorspace", None) @@ -138,19 +140,22 @@ class LoadClip(resolve.TimelineItemLoader): @classmethod def set_item_color(cls, timeline_item, version): - # define version name version_name = version.get("name", None) # get all versions in list - versions = legacy_io.find({ - "type": "version", - "parent": version["parent"] - }).distinct('name') - - max_version = max(versions) + project_name = legacy_io.active_project() + last_version_doc = get_last_version_by_subset_id( + project_name, + version["parent"], + fields=["name"] + ) + if last_version_doc: + last_version = last_version_doc["name"] + else: + last_version = None # set clip colour - if version_name == max_version: + if version_name == last_version: timeline_item.SetClipColor(cls.clip_color_last) else: timeline_item.SetClipColor(cls.clip_color) From 2fd9063de4b92818cc26151243da2e5bdcd4af05 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Jun 2022 11:20:09 +0200 Subject: [PATCH 031/129] use query functions in fusion --- openpype/hosts/fusion/api/lib.py | 53 +++++++++---------- .../hosts/fusion/utility_scripts/switch_ui.py | 13 +++-- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 29f3a3a3eb..001eb636ee 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -3,9 +3,16 @@ import sys import re import contextlib -from bson.objectid import ObjectId from Qt import QtGui +from openpype.client import ( + get_asset_by_name, + get_subset_by_name, + get_last_version_by_subset_id, + get_representation_by_id, + get_representation_by_name, + get_representation_parents, +) from openpype.pipeline import ( switch_container, legacy_io, @@ -93,13 +100,16 @@ def switch_item(container, raise ValueError("Must have at least one change provided to switch.") # Collect any of current asset, subset and representation if not provided - # so we can use the original name from those. + # so we can use the original name from those. + project_name = legacy_io.active_project() if any(not x for x in [asset_name, subset_name, representation_name]): - _id = ObjectId(container["representation"]) - representation = legacy_io.find_one({ - "type": "representation", "_id": _id - }) - version, subset, asset, project = legacy_io.parenthood(representation) + repre_id = container["representation"] + representation = get_representation_by_id(project_name, repre_id) + repre_parent_docs = get_representation_parents(representation) + if repre_parent_docs: + version, subset, asset, _ = repre_parent_docs + else: + version = subset = asset = None if asset_name is None: asset_name = asset["name"] @@ -111,39 +121,26 @@ def switch_item(container, representation_name = representation["name"] # Find the new one - asset = legacy_io.find_one({ - "name": asset_name, - "type": "asset" - }) + asset = get_asset_by_name(project_name, asset_name, fields=["_id"]) assert asset, ("Could not find asset in the database with the name " "'%s'" % asset_name) - subset = legacy_io.find_one({ - "name": subset_name, - "type": "subset", - "parent": asset["_id"] - }) + subset = get_subset_by_name( + project_name, subset_name, asset["_id"], fields=["_id"] + ) assert subset, ("Could not find subset in the database with the name " "'%s'" % subset_name) - version = legacy_io.find_one( - { - "type": "version", - "parent": subset["_id"] - }, - sort=[('name', -1)] + version = get_last_version_by_subset_id( + project_name, subset["_id"], fields=["_id"] ) - assert version, "Could not find a version for {}.{}".format( asset_name, subset_name ) - representation = legacy_io.find_one({ - "name": representation_name, - "type": "representation", - "parent": version["_id"]} + representation = get_representation_by_name( + project_name, representation_name, version["_id"] ) - assert representation, ("Could not find representation in the database " "with the name '%s'" % representation_name) diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/utility_scripts/switch_ui.py index 70eb3d0a19..01d55db647 100644 --- a/openpype/hosts/fusion/utility_scripts/switch_ui.py +++ b/openpype/hosts/fusion/utility_scripts/switch_ui.py @@ -7,6 +7,7 @@ from Qt import QtWidgets, QtCore import qtawesome as qta +from openpype.client import get_assets from openpype import style from openpype.pipeline import ( install_host, @@ -142,7 +143,7 @@ class App(QtWidgets.QWidget): # Clear any existing items self._assets.clear() - asset_names = [a["name"] for a in self.collect_assets()] + asset_names = self.collect_asset_names() completer = QtWidgets.QCompleter(asset_names) self._assets.setCompleter(completer) @@ -165,8 +166,14 @@ class App(QtWidgets.QWidget): items = glob.glob("{}/*.comp".format(directory)) return items - def collect_assets(self): - return list(legacy_io.find({"type": "asset"}, {"name": True})) + def collect_asset_names(self): + project_name = legacy_io.active_project() + asset_docs = get_assets(project_name, fields=["name"]) + asset_names = { + asset_doc["name"] + for asset_doc in asset_docs + } + return list(asset_names) def populate_comp_box(self, files): """Ensure we display the filename only but the path is stored as well From 645b89ced5419d6f74ff5c5782981a72d0e70df1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Jun 2022 11:38:18 +0200 Subject: [PATCH 032/129] use query functions in remaining places --- .../hosts/fusion/plugins/load/load_sequence.py | 7 +++---- .../hosts/fusion/scripts/fusion_switch_shot.py | 16 +++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index b860abd88b..9baa652b60 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -1,6 +1,7 @@ import os import contextlib +from openpype.client import get_version_by_id from openpype.pipeline import ( load, legacy_io, @@ -211,10 +212,8 @@ class FusionLoadSequence(load.LoaderPlugin): path = self._get_first_image(root) # Get start frame from version data - version = legacy_io.find_one({ - "type": "version", - "_id": representation["parent"] - }) + project_name = legacy_io.active_project() + version = get_version_by_id(project_name, representation["parent"]) start = version["data"].get("frameStart") if start is None: self.log.warning("Missing start frame for updated version" diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py index 704f420796..52a157c56e 100644 --- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py +++ b/openpype/hosts/fusion/scripts/fusion_switch_shot.py @@ -4,6 +4,11 @@ import sys import logging # Pipeline imports +from openpype.client import ( + get_project, + get_asset_by_name, + get_versions, +) from openpype.pipeline import ( legacy_io, install_host, @@ -164,9 +169,9 @@ def update_frame_range(comp, representations): """ - version_ids = [r["parent"] for r in representations] - versions = legacy_io.find({"type": "version", "_id": {"$in": version_ids}}) - versions = list(versions) + project_name = legacy_io.active_project() + version_ids = {r["parent"] for r in representations} + versions = list(get_versions(project_name, version_ids)) versions = [v for v in versions if v["data"].get("frameStart", None) is not None] @@ -203,11 +208,12 @@ def switch(asset_name, filepath=None, new=True): # Assert asset name exists # It is better to do this here then to wait till switch_shot does it - asset = legacy_io.find_one({"type": "asset", "name": asset_name}) + project_name = legacy_io.active_project() + asset = get_asset_by_name(project_name, asset_name) assert asset, "Could not find '%s' in the database" % asset_name # Get current project - self._project = legacy_io.find_one({"type": "project"}) + self._project = get_project(project_name) # Go to comp if not filepath: From 45d228ce9fdc0791c79b079782613136f2ff7bc0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 22 Jun 2022 01:07:45 +0200 Subject: [PATCH 033/129] :bug: fix handling of extra geometry --- .../plugins/publish/extract_camera_alembic.py | 6 ++--- .../publish/extract_camera_mayaScene.py | 24 +++++++++++-------- .../publish/validate_camera_contents.py | 13 +--------- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py index 4110ad474d..893aa63b01 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py @@ -61,10 +61,10 @@ class ExtractCameraAlembic(openpype.api.Extractor): if bake_to_worldspace: job_str += ' -worldSpace' - for member in member_shapes: - self.log.info(f"processing {member}") + for member in members: transform = cmds.listRelatives( - member, parent=True, fullPath=True)[0] + member, parent=True, fullPath=True) + transform = transform[0] if transform else member job_str += ' -root {0}'.format(transform) job_str += ' -file "{0}"'.format(path) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py index 1cb30e65ea..569efbe335 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py @@ -172,18 +172,22 @@ class ExtractCameraMayaScene(openpype.api.Extractor): dag=True, shapes=True, long=True) + attrs = {"backgroundColorR": 0.0, + "backgroundColorG": 0.0, + "backgroundColorB": 0.0, + "overscan": 1.0} + # Fix PLN-178: Don't allow background color to be non-black - for cam in cmds.ls( + for cam, (attr, value) in itertools.product(cmds.ls( baked_camera_shapes, type="camera", dag=True, - shapes=True, long=True): - attrs = {"backgroundColorR": 0.0, - "backgroundColorG": 0.0, - "backgroundColorB": 0.0, - "overscan": 1.0} - for attr, value in attrs.items(): - plug = "{0}.{1}".format(cam, attr) - unlock(plug) - cmds.setAttr(plug, value) + shapes=True, long=True), attrs.items()): + # the above call still pull in shapes that are not + # cameras, so we filter them out here + if cmds.nodeType(cam) != "camera": + continue + plug = "{0}.{1}".format(cam, attr) + unlock(plug) + cmds.setAttr(plug, value) self.log.info("Performing extraction..") cmds.select(cmds.ls(members, dag=True, diff --git a/openpype/hosts/maya/plugins/publish/validate_camera_contents.py b/openpype/hosts/maya/plugins/publish/validate_camera_contents.py index eb93245f93..e209487ae4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_camera_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_camera_contents.py @@ -52,20 +52,9 @@ class ValidateCameraContents(pyblish.api.InstancePlugin): if not cls.validate_shapes: cls.log.info("not validating shapes in the content") - - for member in members: - parents = cmds.ls(member, long=True)[0].split("|")[1:-1] - cls.log.info(parents) - parents_long_named = [ - "|".join(parents[:i]) for i in range(1, 1 + len(parents)) - ] - cls.log.info(parents_long_named) - if cameras[0] in parents_long_named: - cls.log.error( - "{} is parented under camera {}".format(member, cameras[0])) - invalid.extend(member) return invalid + # non-camera shapes valid_shapes = cmds.ls(shapes, type=('camera', 'locator'), long=True) shapes = set(shapes) - set(valid_shapes) From 8d849b7e08139b4e1a13db79f41cf35f43ef7893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 22 Jun 2022 14:13:32 +0200 Subject: [PATCH 034/129] :recycle: remove unnecessary check --- .../hosts/maya/plugins/publish/extract_camera_mayaScene.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py index ac696ebdef..8d6c4b5f3c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py @@ -181,16 +181,11 @@ class ExtractCameraMayaScene(openpype.api.Extractor): # Fix PLN-178: Don't allow background color to be non-black for cam, (attr, value) in itertools.product(cmds.ls( baked_camera_shapes, type="camera", dag=True, - shapes=True, long=True), attrs.items()): - # the above call still pull in shapes that are not - # cameras, so we filter them out here - if cmds.nodeType(cam) != "camera": - continue + long=True), attrs.items()): plug = "{0}.{1}".format(cam, attr) unlock(plug) cmds.setAttr(plug, value) - self.log.info("Performing extraction..") cmds.select(cmds.ls(members, dag=True, shapes=True, long=True), noExpand=True) From 796348d4afc38a29eac73c97484d516eb5ba774b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Jun 2022 18:05:37 +0200 Subject: [PATCH 035/129] 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 036/129] 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 037/129] 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 4a8dfc3db9988b61572aa307fe01b8fe1e011e8e Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 16 Jun 2022 14:15:26 +0200 Subject: [PATCH 038/129] blender install pyside2 for all platforms --- .../hosts/blender/hooks/pre_pyside_install.py | 112 ++++++++++++------ 1 file changed, 75 insertions(+), 37 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index a37f8f0379..aaa545b2cf 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -1,6 +1,7 @@ import os import re import subprocess +from platform import system from openpype.lib import PreLaunchHook @@ -13,12 +14,9 @@ class InstallPySideToBlender(PreLaunchHook): For pipeline implementation is required to have Qt binding installed in blender's python packages. - - Prelaunch hook can work only on Windows right now. """ app_groups = ["blender"] - platforms = ["windows"] def execute(self): # Prelaunch hook is not crucial @@ -34,25 +32,28 @@ class InstallPySideToBlender(PreLaunchHook): # Get blender's python directory version_regex = re.compile(r"^[2-3]\.[0-9]+$") + platform = system().lower() executable = self.launch_context.executable.executable_path - if os.path.basename(executable).lower() != "blender.exe": + expected_executable = "blender" + if platform == "windows": + expected_executable += ".exe" + + if os.path.basename(executable).lower() != expected_executable: self.log.info(( - "Executable does not lead to blender.exe file. Can't determine" - " blender's python to check/install PySide2." + f"Executable does not lead to {expected_executable} file." + "Can't determine blender's python to check/install PySide2." )) return - executable_dir = os.path.dirname(executable) + versions_dir = os.path.dirname(executable) + if platform == "darwin": + versions_dir = os.path.join( + os.path.dirname(versions_dir), "Resources" + ) version_subfolders = [] - for name in os.listdir(executable_dir): - fullpath = os.path.join(name, executable_dir) - if not os.path.isdir(fullpath): - continue - - if not version_regex.match(name): - continue - - version_subfolders.append(name) + for dir_entry in os.scandir(versions_dir): + if dir_entry.is_dir() and version_regex.match(dir_entry.name): + version_subfolders.append(dir_entry.name) if not version_subfolders: self.log.info( @@ -72,16 +73,21 @@ class InstallPySideToBlender(PreLaunchHook): version_subfolder = version_subfolders[0] - pythond_dir = os.path.join( - os.path.dirname(executable), - version_subfolder, - "python" - ) + python_dir = os.path.join(versions_dir, version_subfolder, "python") + python_lib = os.path.join(python_dir, "lib") + python_version = "python" + + if platform != "windows": + for dir_entry in os.scandir(python_lib): + if dir_entry.is_dir() and dir_entry.name.startswith("python"): + python_lib = dir_entry.path + python_version = dir_entry.name + break # Change PYTHONPATH to contain blender's packages as first python_paths = [ - os.path.join(pythond_dir, "lib"), - os.path.join(pythond_dir, "lib", "site-packages"), + python_lib, + os.path.join(python_lib, "site-packages"), ] python_path = self.launch_context.env.get("PYTHONPATH") or "" for path in python_path.split(os.pathsep): @@ -91,7 +97,12 @@ class InstallPySideToBlender(PreLaunchHook): self.launch_context.env["PYTHONPATH"] = os.pathsep.join(python_paths) # Get blender's python executable - python_executable = os.path.join(pythond_dir, "bin", "python.exe") + python_bin = os.path.join(python_dir, "bin") + if platform == "windows": + python_executable = os.path.join(python_bin, "python.exe") + else: + python_executable = os.path.join(python_bin, python_version) + if not os.path.exists(python_executable): self.log.warning( "Couldn't find python executable for blender. {}".format( @@ -106,7 +117,15 @@ class InstallPySideToBlender(PreLaunchHook): return # Install PySide2 in blender's python - self.install_pyside_windows(python_executable) + if platform == "windows": + result = self.install_pyside_windows(python_executable) + else: + result = self.install_pyside(python_executable) + + if result: + self.log.info("Successfully installed PySide2 module to blender.") + else: + self.log.warning("Failed to install PySide2 module to blender.") def install_pyside_windows(self, python_executable): """Install PySide2 python module to blender's python. @@ -144,21 +163,38 @@ class InstallPySideToBlender(PreLaunchHook): lpDirectory=os.path.dirname(python_executable) ) process_handle = process_info["hProcess"] - obj = win32event.WaitForSingleObject( - process_handle, win32event.INFINITE - ) + win32event.WaitForSingleObject(process_handle, win32event.INFINITE) returncode = win32process.GetExitCodeProcess(process_handle) - if returncode == 0: - self.log.info( - "Successfully installed PySide2 module to blender." - ) - return + return returncode == 0 except pywintypes.error: pass - self.log.warning("Failed to install PySide2 module to blender.") + @staticmethod + def install_pyside(python_executable): + """Install PySide2 python module to blender's python.""" + try: + # Parameters + # - use "-m pip" as module pip to install PySide2 and argument + # "--ignore-installed" is to force install module to blender's + # site-packages and make sure it is binary compatible + args = [ + python_executable, + "-m", + "pip", + "install", + "--ignore-installed", + "PySide2", + ] + process = subprocess.Popen( + args, stdout=subprocess.PIPE, universal_newlines=True + ) + stdout, _ = process.communicate() + return process.returncode == 0 + except subprocess.SubprocessError: + pass - def is_pyside_installed(self, python_executable): + @staticmethod + def is_pyside_installed(python_executable): """Check if PySide2 module is in blender's pip list. Check that PySide2 is installed directly in blender's site-packages. @@ -167,9 +203,11 @@ class InstallPySideToBlender(PreLaunchHook): """ # Get pip list from blender's python executable args = [python_executable, "-m", "pip", "list"] - process = subprocess.Popen(args, stdout=subprocess.PIPE) + process = subprocess.Popen( + args, stdout=subprocess.PIPE, universal_newlines=True + ) stdout, _ = process.communicate() - lines = stdout.decode().split("\r\n") + lines = stdout.split(os.linesep) # Second line contain dashes that define maximum length of module name. # Second column of dashes define maximum length of module version. package_dashes, *_ = lines[1].split(" ") From b42505b430fa77608c722d91fce2f50ff38ba5e6 Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Tue, 21 Jun 2022 15:19:34 +0200 Subject: [PATCH 039/129] fix pre_pyside_install for all platform --- openpype/hosts/blender/hooks/pre_pyside_install.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index aaa545b2cf..d2c07e860d 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -203,11 +203,9 @@ class InstallPySideToBlender(PreLaunchHook): """ # Get pip list from blender's python executable args = [python_executable, "-m", "pip", "list"] - process = subprocess.Popen( - args, stdout=subprocess.PIPE, universal_newlines=True - ) + process = subprocess.Popen(args, stdout=subprocess.PIPE) stdout, _ = process.communicate() - lines = stdout.split(os.linesep) + lines = stdout.decode().split(os.linesep) # Second line contain dashes that define maximum length of module name. # Second column of dashes define maximum length of module version. package_dashes, *_ = lines[1].split(" ") From e36c80fd09adea0322bc03c3c4da1764cc620d62 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 15:41:41 +0200 Subject: [PATCH 040/129] 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 07e67a14b76047129fa0dcf49ec1be452c6af79b Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 23 Jun 2022 16:05:46 +0200 Subject: [PATCH 041/129] more exceptions for pre pisyde install --- openpype/hosts/blender/hooks/pre_pyside_install.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index d2c07e860d..9e1046453b 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -169,8 +169,7 @@ class InstallPySideToBlender(PreLaunchHook): except pywintypes.error: pass - @staticmethod - def install_pyside(python_executable): + def install_pyside(self, python_executable): """Install PySide2 python module to blender's python.""" try: # Parameters @@ -188,13 +187,16 @@ class InstallPySideToBlender(PreLaunchHook): process = subprocess.Popen( args, stdout=subprocess.PIPE, universal_newlines=True ) - stdout, _ = process.communicate() + process.communicate() return process.returncode == 0 + except PermissionError: + self.log.warning("Permission denied with command: \"{}\".".format(" ".join(args))) + except OSError as error: + self.log.warning("OS error has occurred with command: \"{error}\".") except subprocess.SubprocessError: pass - @staticmethod - def is_pyside_installed(python_executable): + def is_pyside_installed(self, python_executable): """Check if PySide2 module is in blender's pip list. Check that PySide2 is installed directly in blender's site-packages. From 262d179e15e1236f09fd86352a697eaff23a899d Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Thu, 23 Jun 2022 16:09:50 +0200 Subject: [PATCH 042/129] fix last commit draft code --- openpype/hosts/blender/hooks/pre_pyside_install.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index 9e1046453b..d0f2b3d417 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -190,9 +190,12 @@ class InstallPySideToBlender(PreLaunchHook): process.communicate() return process.returncode == 0 except PermissionError: - self.log.warning("Permission denied with command: \"{}\".".format(" ".join(args))) + self.log.warning( + "Permission denied with command:" + "\"{}\".".format(" ".join(args)) + ) except OSError as error: - self.log.warning("OS error has occurred with command: \"{error}\".") + self.log.warning(f"OS error has occurred: \"{error}\".") except subprocess.SubprocessError: pass From bf592641e870556c474da160b1e107a3377388a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 17:13:29 +0200 Subject: [PATCH 043/129] 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 044/129] 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 045/129] 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 18859001b137ec61c281a5b7750f456f19abb27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 23 Jun 2022 17:23:34 +0200 Subject: [PATCH 046/129] :bug: keep baked camera out of hierarchy --- .../plugins/publish/extract_camera_alembic.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py index a8e13e645f..054aadcbee 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py @@ -30,7 +30,7 @@ class ExtractCameraAlembic(openpype.api.Extractor): # get cameras members = instance.data['setMembers'] - cameras = cmds.ls(members, leaf=True, shapes=True, long=True, + cameras = cmds.ls(members, leaf=True, long=True, dag=True, type="camera") # validate required settings @@ -62,7 +62,21 @@ class ExtractCameraAlembic(openpype.api.Extractor): if bake_to_worldspace: job_str += ' -worldSpace' + # if baked, drop the camera hierarchy to maintain + # clean output and backwards compatibility + camera_root = cmds.listRelatives( + camera, parent=True, fullPath=True)[0] + job_str += ' -root {0}'.format(camera_root) + for member in members: + member_content = cmds.listRelatives( + member, ad=True, fullPath=True) or [] + # skip hierarchy if it contains only camera + # `member_content` will contain camera + its parents + if camera in member_content \ + and len(member_content) == len(camera.split("|")) - 2: # noqa + continue + transform = cmds.listRelatives( member, parent=True, fullPath=True) transform = transform[0] if transform else member From 0c1688eaa5d64de6935b8fbcbe43ea8074d5b443 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 24 Jun 2022 11:06:57 +0300 Subject: [PATCH 047/129] Fix comparison statements. --- .../maya/plugins/publish/extract_playblast.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 32cbeed81b..4ba8bd5976 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -56,23 +56,23 @@ class ExtractPlayblast(openpype.api.Extractor): width_preset = capture_presets["Resolution"]["width"] height_preset = capture_presets["Resolution"]["height"] # Set resolution variables from instance values - instance_width = instance.data.get("resolutionWidth") - instance_height = instance.data.get("resolutionHeight") + instance_width = instance.context.data.get("resolutionWidth") + instance_height = instance.context.data.get("resolutionHeight") preset['camera'] = camera # Tests if instance resolution width is set, # if it is a value other than zero, that value is # used, if not then the project settings resolution is # used - if instance_width != 0: - preset['width'] = instance.data.get("resolutionWidth") - else: + if width_preset != 0: preset["width"] = width_preset - - if instance_height != 0: - preset['height'] = instance.data.get("resolutionHeight") else: + preset['width'] = instance_width + + if height_preset != 0: preset['height'] = height_preset + else: + preset['height'] = instance_height preset['start_frame'] = start preset['end_frame'] = end From c6d473f6b0354ebc7beffd0641eef3d8ea6a79b1 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 24 Jun 2022 11:08:11 +0300 Subject: [PATCH 048/129] Fix comments. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 4ba8bd5976..8378b5c22a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -55,14 +55,14 @@ class ExtractPlayblast(openpype.api.Extractor): # Set resolution variables from capture presets width_preset = capture_presets["Resolution"]["width"] height_preset = capture_presets["Resolution"]["height"] - # Set resolution variables from instance values + # Set resolution variables from asset values instance_width = instance.context.data.get("resolutionWidth") instance_height = instance.context.data.get("resolutionHeight") preset['camera'] = camera - # Tests if instance resolution width is set, + # Tests if project resolution is set, # if it is a value other than zero, that value is - # used, if not then the project settings resolution is + # used, if not then the asset resolution is # used if width_preset != 0: preset["width"] = width_preset From 3968e1607e4c08e31c16d731c8e3d7fdafcd01d6 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 24 Jun 2022 14:10:29 +0300 Subject: [PATCH 049/129] Change logic into correct setting. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 8378b5c22a..ff15546033 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -65,14 +65,14 @@ class ExtractPlayblast(openpype.api.Extractor): # used, if not then the asset resolution is # used if width_preset != 0: - preset["width"] = width_preset + preset["width"] = instance_width else: - preset['width'] = instance_width + preset['width'] = width_preset if height_preset != 0: - preset['height'] = height_preset - else: preset['height'] = instance_height + else: + preset['height'] = height_preset preset['start_frame'] = start preset['end_frame'] = end From 07b708bcdd3baa9004f6566d15ce8a378b47a528 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 24 Jun 2022 13:26:24 +0200 Subject: [PATCH 050/129] added plate to supported families --- openpype/hosts/fusion/plugins/load/load_sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index b860abd88b..3e28f3e411 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -123,7 +123,7 @@ def loader_shift(loader, frame, relative=True): class FusionLoadSequence(load.LoaderPlugin): """Load image sequence into Fusion""" - families = ["imagesequence", "review", "render"] + families = ["imagesequence", "review", "render", "plate"] representations = ["*"] label = "Load sequence" From 9778d2747b8a09754778d50cf1d1af6af643dd14 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 24 Jun 2022 14:30:32 +0300 Subject: [PATCH 051/129] Revert "Change logic into correct setting." This reverts commit 3968e1607e4c08e31c16d731c8e3d7fdafcd01d6. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index ff15546033..8378b5c22a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -65,14 +65,14 @@ class ExtractPlayblast(openpype.api.Extractor): # used, if not then the asset resolution is # used if width_preset != 0: - preset["width"] = instance_width + preset["width"] = width_preset else: - preset['width'] = width_preset + preset['width'] = instance_width if height_preset != 0: - preset['height'] = instance_height - else: preset['height'] = height_preset + else: + preset['height'] = instance_height preset['start_frame'] = start preset['end_frame'] = end From 0b07036fa832adb4e408b3797060bd9d7026ede3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Jun 2022 17:34:14 +0200 Subject: [PATCH 052/129] :recycle: use sets for member comparsion --- .../plugins/publish/extract_camera_alembic.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py index 054aadcbee..b744bfd0fe 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py @@ -69,14 +69,19 @@ class ExtractCameraAlembic(openpype.api.Extractor): job_str += ' -root {0}'.format(camera_root) for member in members: - member_content = cmds.listRelatives( - member, ad=True, fullPath=True) or [] - # skip hierarchy if it contains only camera - # `member_content` will contain camera + its parents - if camera in member_content \ - and len(member_content) == len(camera.split("|")) - 2: # noqa - continue - + descendants = cmds.listRelatives(member, + allDescendents=True, + fullPath=True) or [] + shapes = cmds.ls(descendants, shapes=True, + noIntermediate=True, long=True) + cameras = cmds.ls(shapes, type="camera", long=True) + if cameras: + if not set(shapes) - set(cameras): + continue + self.log.warning(( + "Camera hierarchy contains additional geometry. " + "Extraction will fail.") + ) transform = cmds.listRelatives( member, parent=True, fullPath=True) transform = transform[0] if transform else member From fcefcc74348f7f7be89c3d34caa4ca88819dbd1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 24 Jun 2022 17:52:29 +0200 Subject: [PATCH 053/129] 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 054/129] 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 b4817f70a6b2058aff705ebc5dcae67ff2965c15 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 24 Jun 2022 19:11:21 +0200 Subject: [PATCH 055/129] show what is allowed to drop in the files widget --- openpype/style/style.css | 3 + .../widgets/attribute_defs/files_widget.py | 125 ++++++++++++++++-- 2 files changed, 117 insertions(+), 11 deletions(-) diff --git a/openpype/style/style.css b/openpype/style/style.css index d76d833be1..72d12a9230 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1418,3 +1418,6 @@ InViewButton, InViewButton:disabled { InViewButton:hover { background: rgba(255, 255, 255, 37); } +SupportLabel { + color: {color:font-disabled}; +} diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 23cf8342b1..24e3f4bb25 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -26,26 +26,122 @@ IS_SEQUENCE_ROLE = QtCore.Qt.UserRole + 7 EXT_ROLE = QtCore.Qt.UserRole + 8 +class SupportLabel(QtWidgets.QLabel): + pass + + class DropEmpty(QtWidgets.QWidget): - _drop_enabled_text = "Drag & Drop\n(drop files here)" + _empty_extensions = "Any file" - def __init__(self, parent): + def __init__(self, single_item, allow_sequences, parent): super(DropEmpty, self).__init__(parent) - label_widget = QtWidgets.QLabel(self._drop_enabled_text, self) - label_widget.setAlignment(QtCore.Qt.AlignCenter) - label_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + drop_label_widget = QtWidgets.QLabel("Drag & Drop files here", self) - layout = QtWidgets.QHBoxLayout(self) + detail_widget = QtWidgets.QWidget(self) + items_label_widget = SupportLabel(detail_widget) + extensions_label_widget = SupportLabel(detail_widget) + extensions_label_widget.setWordWrap(True) + + detail_layout = QtWidgets.QVBoxLayout(detail_widget) + detail_layout.setContentsMargins(0, 0, 0, 0) + detail_layout.addStretch(1) + detail_layout.addWidget( + items_label_widget, 0, alignment=QtCore.Qt.AlignCenter + ) + detail_layout.addWidget( + extensions_label_widget, 0, alignment=QtCore.Qt.AlignCenter + ) + + layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addSpacing(10) layout.addWidget( - label_widget, - alignment=QtCore.Qt.AlignCenter + drop_label_widget, 0, alignment=QtCore.Qt.AlignCenter ) + layout.addWidget(detail_widget, 1) layout.addSpacing(10) - self._label_widget = label_widget + for widget in ( + detail_widget, + drop_label_widget, + items_label_widget, + extensions_label_widget, + ): + if isinstance(widget, QtWidgets.QLabel): + widget.setAlignment(QtCore.Qt.AlignCenter) + widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self._single_item = single_item + self._allow_sequences = allow_sequences + self._allowed_extensions = set() + self._allow_folders = None + + self._drop_label_widget = drop_label_widget + self._items_label_widget = items_label_widget + self._extensions_label_widget = extensions_label_widget + + self.set_allow_folders(False) + + def set_extensions(self, extensions): + if extensions: + extensions = { + ext.replace(".", "") + for ext in extensions + } + if extensions == self._allowed_extensions: + return + self._allowed_extensions = extensions + + self._update_items_label() + + def set_allow_folders(self, allowed): + if self._allow_folders == allowed: + return + + self._allow_folders = allowed + self._update_items_label() + + def _update_items_label(self): + extensions_label = "" + if self._allowed_extensions: + extensions_label = ", ".join(sorted(self._allowed_extensions)) + + allowed_items = [] + if self._allow_folders: + allowed_items.append("folder") + + if extensions_label: + allowed_items.append("file") + if self._allow_sequences: + allowed_items.append("sequence") + + num_label = "Single" + if not self._single_item: + num_label = "Multiple" + allowed_items = [item + "s" for item in allowed_items] + + if not allowed_items: + allowed_items_label = "" + elif len(allowed_items) == 1: + allowed_items_label = allowed_items[0] + elif len(allowed_items) == 2: + allowed_items_label = " or ".join(allowed_items) + else: + last_item = allowed_items.pop(-1) + new_last_item = " or ".join(last_item, allowed_items.pop(-1)) + allowed_items.append(new_last_item) + allowed_items_label = ", ".join(allowed_items) + + if allowed_items_label: + items_label = "{} {}".format(num_label, allowed_items_label) + if extensions_label: + items_label += " of" + else: + items_label = "It is not allowed to add anything here!" + + self._items_label_widget.setText(items_label) + self._extensions_label_widget.setText(extensions_label) def paintEvent(self, event): super(DropEmpty, self).paintEvent(event) @@ -188,7 +284,12 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): def set_allowed_extensions(self, extensions=None): if extensions is not None: - extensions = set(extensions) + _extensions = set() + for ext in set(extensions): + if not ext.startswith("."): + ext = ".{}".format(ext) + _extensions.add(ext.lower()) + extensions = _extensions if self._allowed_extensions != extensions: self._allowed_extensions = extensions @@ -444,7 +545,7 @@ class FilesWidget(QtWidgets.QFrame): super(FilesWidget, self).__init__(parent) self.setAcceptDrops(True) - empty_widget = DropEmpty(self) + empty_widget = DropEmpty(single_item, allow_sequences, self) files_model = FilesModel(single_item, allow_sequences) files_proxy_model = FilesProxyModel() @@ -519,6 +620,8 @@ class FilesWidget(QtWidgets.QFrame): def set_filters(self, folders_allowed, exts_filter): self._files_proxy_model.set_allow_folders(folders_allowed) self._files_proxy_model.set_allowed_extensions(exts_filter) + self._empty_widget.set_extensions(exts_filter) + self._empty_widget.set_allow_folders(folders_allowed) def _on_rows_inserted(self, parent_index, start_row, end_row): for row in range(start_row, end_row + 1): From 462807c2726b0cff2e351db8edc759114fb52cc3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 24 Jun 2022 19:19:25 +0200 Subject: [PATCH 056/129] removed unnecessary widgets --- .../widgets/attribute_defs/files_widget.py | 59 ++++++++----------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 24e3f4bb25..af5a1d130b 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -38,20 +38,8 @@ class DropEmpty(QtWidgets.QWidget): drop_label_widget = QtWidgets.QLabel("Drag & Drop files here", self) - detail_widget = QtWidgets.QWidget(self) - items_label_widget = SupportLabel(detail_widget) - extensions_label_widget = SupportLabel(detail_widget) - extensions_label_widget.setWordWrap(True) - - detail_layout = QtWidgets.QVBoxLayout(detail_widget) - detail_layout.setContentsMargins(0, 0, 0, 0) - detail_layout.addStretch(1) - detail_layout.addWidget( - items_label_widget, 0, alignment=QtCore.Qt.AlignCenter - ) - detail_layout.addWidget( - extensions_label_widget, 0, alignment=QtCore.Qt.AlignCenter - ) + items_label_widget = SupportLabel(self) + items_label_widget.setWordWrap(True) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -59,17 +47,17 @@ class DropEmpty(QtWidgets.QWidget): layout.addWidget( drop_label_widget, 0, alignment=QtCore.Qt.AlignCenter ) - layout.addWidget(detail_widget, 1) + layout.addStretch(1) + layout.addWidget( + items_label_widget, 0, alignment=QtCore.Qt.AlignCenter + ) layout.addSpacing(10) for widget in ( - detail_widget, drop_label_widget, items_label_widget, - extensions_label_widget, ): - if isinstance(widget, QtWidgets.QLabel): - widget.setAlignment(QtCore.Qt.AlignCenter) + widget.setAlignment(QtCore.Qt.AlignCenter) widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) self._single_item = single_item @@ -79,7 +67,6 @@ class DropEmpty(QtWidgets.QWidget): self._drop_label_widget = drop_label_widget self._items_label_widget = items_label_widget - self._extensions_label_widget = extensions_label_widget self.set_allow_folders(False) @@ -103,27 +90,29 @@ class DropEmpty(QtWidgets.QWidget): self._update_items_label() def _update_items_label(self): - extensions_label = "" - if self._allowed_extensions: - extensions_label = ", ".join(sorted(self._allowed_extensions)) - allowed_items = [] if self._allow_folders: allowed_items.append("folder") - if extensions_label: + if self._allowed_extensions: allowed_items.append("file") if self._allow_sequences: allowed_items.append("sequence") - num_label = "Single" if not self._single_item: - num_label = "Multiple" allowed_items = [item + "s" for item in allowed_items] if not allowed_items: - allowed_items_label = "" - elif len(allowed_items) == 1: + self._items_label_widget.setText( + "It is not allowed to add anything here!" + ) + return + + items_label = "Multiple " + if self._single_item: + items_label = "Single " + + if len(allowed_items) == 1: allowed_items_label = allowed_items[0] elif len(allowed_items) == 2: allowed_items_label = " or ".join(allowed_items) @@ -133,15 +122,13 @@ class DropEmpty(QtWidgets.QWidget): allowed_items.append(new_last_item) allowed_items_label = ", ".join(allowed_items) - if allowed_items_label: - items_label = "{} {}".format(num_label, allowed_items_label) - if extensions_label: - items_label += " of" - else: - items_label = "It is not allowed to add anything here!" + items_label += allowed_items_label + if self._allowed_extensions: + items_label += " of\n{}".format( + ", ".join(sorted(self._allowed_extensions)) + ) self._items_label_widget.setText(items_label) - self._extensions_label_widget.setText(extensions_label) def paintEvent(self, event): super(DropEmpty, self).paintEvent(event) From 46bfbd28506be577532cb68f319a3051e63957ee Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 27 Jun 2022 06:13:35 +0300 Subject: [PATCH 057/129] Make appropriate feature fixes. --- .../maya/plugins/create/create_review.py | 8 +++--- .../maya/plugins/publish/collect_review.py | 4 +-- .../maya/plugins/publish/extract_playblast.py | 26 ++++++++++++------- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 331f0818eb..83b7f34d82 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -15,8 +15,8 @@ class CreateReview(plugin.Creator): keepImages = False isolate = False imagePlane = True - resolutionWidth = 0 - resolutionHeight = 0 + attrWidth = 0 + attrHeight = 0 transparency = [ "preset", "simple", @@ -35,8 +35,8 @@ class CreateReview(plugin.Creator): for key, value in animation_data.items(): data[key] = value - data["resolutionWidth"] = self.resolutionWidth - data["resolutionHeight"] = self.resolutionHeight + data["attrWidth"] = self.attrWidth + data["attrHeight"] = self.attrHeight data["isolate"] = self.isolate data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 0769747205..83c4760535 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -71,8 +71,8 @@ class CollectReview(pyblish.api.InstancePlugin): data['handles'] = instance.data.get('handles', None) data['step'] = instance.data['step'] data['fps'] = instance.data['fps'] - data['resolutionWidth'] = instance.data['resolutionWidth'] - data['resolutionHeight'] = instance.data['resolutionHeight'] + data['attrWidth'] = instance.data['attrWidth'] + data['attrHeight'] = instance.data['attrHeight'] data["isolate"] = instance.data["isolate"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 7cd829dafb..6f7dd5e16a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -1,6 +1,7 @@ import os import glob import contextlib + import clique import capture @@ -56,23 +57,30 @@ class ExtractPlayblast(openpype.api.Extractor): width_preset = capture_presets["Resolution"]["width"] height_preset = capture_presets["Resolution"]["height"] # Set resolution variables from asset values - instance_width = instance.context.data.get("resolutionWidth") - instance_height = instance.context.data.get("resolutionHeight") + asset_width = instance.data.get("resolutionWidth") + asset_height = instance.data.get("resolutionHeight") + review_instance_width = instance.data.get("attrWidth") + review_instance_height = instance.data.get("attrHeight") preset['camera'] = camera # Tests if project resolution is set, # if it is a value other than zero, that value is # used, if not then the asset resolution is # used - if width_preset != 0: - preset["width"] = width_preset - else: - preset['width'] = instance_width - if height_preset != 0: + if review_instance_width != 0: + preset['width'] = review_instance_width + elif width_preset == 0: + preset['width'] = asset_width + elif width_preset != 0: + preset['width'] = width_preset + + if review_instance_height != 0: + preset['height'] = review_instance_height + elif height_preset == 0: + preset['height'] = asset_height + elif height_preset != 0: preset['height'] = height_preset - else: - preset['height'] = instance_height preset['start_frame'] = start preset['end_frame'] = end From 37c98c045e7047a6e35ee807ba476a916c04a84b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 10:14:43 +0200 Subject: [PATCH 058/129] added spacing --- openpype/widgets/attribute_defs/files_widget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index af5a1d130b..3135da6691 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -47,6 +47,7 @@ class DropEmpty(QtWidgets.QWidget): layout.addWidget( drop_label_widget, 0, alignment=QtCore.Qt.AlignCenter ) + layout.addSpacing(10) layout.addStretch(1) layout.addWidget( items_label_widget, 0, alignment=QtCore.Qt.AlignCenter From 6af7f906e5b3cde5d76aac36676e2a3ee3f18ac6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 13:19:15 +0200 Subject: [PATCH 059/129] 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 060/129] 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 061/129] 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 1da6eef8d3501644cfb9751e45082aba85899e3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 14:54:20 +0200 Subject: [PATCH 062/129] fix subset name change on change of creator plugin --- openpype/tools/publisher/widgets/create_dialog.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/create_dialog.py b/openpype/tools/publisher/widgets/create_dialog.py index 53bbef8b75..3a68835dc7 100644 --- a/openpype/tools/publisher/widgets/create_dialog.py +++ b/openpype/tools/publisher/widgets/create_dialog.py @@ -977,7 +977,12 @@ class CreateDialog(QtWidgets.QDialog): elif variant: self.variant_hints_menu.addAction(variant) - self.variant_input.setText(default_variant or "Main") + variant_text = default_variant or "Main" + # Make sure subset name is updated to new plugin + if variant_text == self.variant_input.text(): + self._on_variant_change() + else: + self.variant_input.setText(variant_text) def _on_variant_widget_resize(self): self.variant_hints_btn.setFixedHeight(self.variant_input.height()) From edff8ed4005122c90e2f823385327184149647ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 18:25:07 +0200 Subject: [PATCH 063/129] use query functions in load camera --- .../hosts/unreal/plugins/load/load_camera.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index e93be486b0..a61d5642c0 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -6,7 +6,7 @@ import unreal from unreal import EditorAssetLibrary from unreal import EditorLevelLibrary from unreal import EditorLevelUtils - +from openpype.client import get_assets, get_asset_by_name from openpype.pipeline import ( AVALON_CONTAINER_ID, legacy_io, @@ -24,14 +24,6 @@ class CameraLoader(plugin.Loader): icon = "cube" color = "orange" - def _get_data(self, asset_name): - asset_doc = legacy_io.find_one({ - "type": "asset", - "name": asset_name - }) - - return asset_doc.get("data") - def _set_sequence_hierarchy( self, seq_i, seq_j, min_frame_j, max_frame_j ): @@ -177,6 +169,19 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(level) + project_name = legacy_io.active_project() + # TODO refactor + # - variables does not match their meaning + # - why scene is stored to sequences? + # - asset documents vs. elements + # - cleanup variable names in whole function + # - e.g. 'asset', 'asset_name', 'asset_data', 'asset_doc' + # - this loop should be a method + # - really inefficient queries of asset documents + # - it looks like the loader cares about much more then should? + # - existing asset in scene is considered as "with correct values" + # - variable 'elements' is modified during it's loop? + # - separate into more methods (spaghetti) # Get all the sequences in the hierarchy. It will create them, if # they don't exist. sequences = [] @@ -201,26 +206,22 @@ class CameraLoader(plugin.Loader): factory=unreal.LevelSequenceFactoryNew() ) - asset_data = legacy_io.find_one({ - "type": "asset", - "name": h.split('/')[-1] - }) - - id = asset_data.get('_id') + asset_data = get_asset_by_name(project_name, h.split('/')[-1]) start_frames = [] end_frames = [] - elements = list( - legacy_io.find({"type": "asset", "data.visualParent": id})) + elements = list(get_assets( + project_name, parent_ids=[asset_data["_id"]] + )) + for e in elements: start_frames.append(e.get('data').get('clipIn')) end_frames.append(e.get('data').get('clipOut')) - elements.extend(legacy_io.find({ - "type": "asset", - "data.visualParent": e.get('_id') - })) + elements.extend(get_assets( + project_name, parent_ids=[e["_id"]] + )) min_frame = min(start_frames) max_frame = max(end_frames) @@ -256,7 +257,7 @@ class CameraLoader(plugin.Loader): sequences[i], sequences[i + 1], frame_ranges[i + 1][0], frame_ranges[i + 1][1]) - data = self._get_data(asset) + data = get_asset_by_name(project_name, asset)["data"] cam_seq.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) cam_seq.set_playback_start(0) From 447d9eab5c178287ddc011c1ee7d40b1a60d21f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 18:40:35 +0200 Subject: [PATCH 064/129] use query functions in unreal --- .../hosts/unreal/plugins/load/load_camera.py | 16 +++++-- .../hosts/unreal/plugins/load/load_layout.py | 42 +++++++++---------- .../unreal/plugins/publish/extract_layout.py | 16 ++++--- 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index a61d5642c0..15adf8a5d5 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -171,6 +171,8 @@ class CameraLoader(plugin.Loader): project_name = legacy_io.active_project() # TODO refactor + # - Creationg of hierarchy should be a function in unreal integration + # - it's used in multiple loaders but must not be loader's logic # - variables does not match their meaning # - why scene is stored to sequences? # - asset documents vs. elements @@ -206,13 +208,19 @@ class CameraLoader(plugin.Loader): factory=unreal.LevelSequenceFactoryNew() ) - asset_data = get_asset_by_name(project_name, h.split('/')[-1]) + asset_data = get_asset_by_name( + project_name, + h.split('/')[-1], + fields=["_id", "data.fps"] + ) start_frames = [] end_frames = [] elements = list(get_assets( - project_name, parent_ids=[asset_data["_id"]] + project_name, + parent_ids=[asset_data["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] )) for e in elements: @@ -220,7 +228,9 @@ class CameraLoader(plugin.Loader): end_frames.append(e.get('data').get('clipOut')) elements.extend(get_assets( - project_name, parent_ids=[e["_id"]] + project_name, + parent_ids=[e["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] )) min_frame = min(start_frames) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index c65cd25ac8..3f16a68ead 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Loader for layouts.""" -import os import json from pathlib import Path @@ -12,6 +11,7 @@ from unreal import AssetToolsHelpers from unreal import FBXImportType from unreal import MathLibrary as umath +from openpype.client import get_asset_by_name, get_assets from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -88,15 +88,6 @@ class LayoutLoader(plugin.Loader): return None - @staticmethod - def _get_data(asset_name): - asset_doc = legacy_io.find_one({ - "type": "asset", - "name": asset_name - }) - - return asset_doc.get("data") - @staticmethod def _set_sequence_hierarchy( seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths @@ -364,26 +355,30 @@ class LayoutLoader(plugin.Loader): factory=unreal.LevelSequenceFactoryNew() ) - asset_data = legacy_io.find_one({ - "type": "asset", - "name": h_dir.split('/')[-1] - }) - - id = asset_data.get('_id') + project_name = legacy_io.active_project() + asset_data = get_asset_by_name( + project_name, + h_dir.split('/')[-1], + fields=["_id", "data.fps"] + ) start_frames = [] end_frames = [] - elements = list( - legacy_io.find({"type": "asset", "data.visualParent": id})) + elements = list(get_assets( + project_name, + parent_ids=[asset_data["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) for e in elements: start_frames.append(e.get('data').get('clipIn')) end_frames.append(e.get('data').get('clipOut')) - elements.extend(legacy_io.find({ - "type": "asset", - "data.visualParent": e.get('_id') - })) + elements.extend(get_assets( + project_name, + parent_ids=[e["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) min_frame = min(start_frames) max_frame = max(end_frames) @@ -659,7 +654,8 @@ class LayoutLoader(plugin.Loader): frame_ranges[i + 1][0], frame_ranges[i + 1][1], [level]) - data = self._get_data(asset) + project_name = legacy_io.active_project() + data = get_asset_by_name(project_name, asset)["data"] shot.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) shot.set_playback_start(0) diff --git a/openpype/hosts/unreal/plugins/publish/extract_layout.py b/openpype/hosts/unreal/plugins/publish/extract_layout.py index 87e6693a97..8924df36a7 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_layout.py +++ b/openpype/hosts/unreal/plugins/publish/extract_layout.py @@ -9,6 +9,7 @@ import unreal from unreal import EditorLevelLibrary as ell from unreal import EditorAssetLibrary as eal +from openpype.client import get_representation_by_name import openpype.api from openpype.pipeline import legacy_io @@ -34,6 +35,7 @@ class ExtractLayout(openpype.api.Extractor): "Wrong level loaded" json_data = [] + project_name = legacy_io.active_project() for member in instance[:]: actor = ell.get_actor_reference(member) @@ -57,17 +59,13 @@ class ExtractLayout(openpype.api.Extractor): self.log.error("AssetContainer not found.") return - parent = eal.get_metadata_tag(asset_container, "parent") + parent_id = eal.get_metadata_tag(asset_container, "parent") family = eal.get_metadata_tag(asset_container, "family") - self.log.info("Parent: {}".format(parent)) - blend = legacy_io.find_one( - { - "type": "representation", - "parent": ObjectId(parent), - "name": "blend" - }, - projection={"_id": True}) + self.log.info("Parent: {}".format(parent_id)) + blend = get_representation_by_name( + project_name, "blend", parent_id, fields=["_id"] + ) blend_id = blend["_id"] json_element = {} From e7883fbbcca294a7b3a78e2da82445110b0545a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 18:42:02 +0200 Subject: [PATCH 065/129] removed unused import --- openpype/hosts/unreal/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index a3e125a94e..950799cc10 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,6 +1,5 @@ import unreal -from openpype.pipeline import legacy_io from openpype.hosts.unreal.api import pipeline from openpype.hosts.unreal.api.plugin import Creator From 12eb1cc53b0e3b6023bf03596719671496edd909 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 18:51:10 +0200 Subject: [PATCH 066/129] modified comments --- openpype/hosts/unreal/plugins/load/load_camera.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 15adf8a5d5..ca6b0ce736 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -173,17 +173,15 @@ class CameraLoader(plugin.Loader): # TODO refactor # - Creationg of hierarchy should be a function in unreal integration # - it's used in multiple loaders but must not be loader's logic + # - hard to say what is purpose of the loop # - variables does not match their meaning # - why scene is stored to sequences? # - asset documents vs. elements # - cleanup variable names in whole function # - e.g. 'asset', 'asset_name', 'asset_data', 'asset_doc' - # - this loop should be a method # - really inefficient queries of asset documents - # - it looks like the loader cares about much more then should? # - existing asset in scene is considered as "with correct values" - # - variable 'elements' is modified during it's loop? - # - separate into more methods (spaghetti) + # - variable 'elements' is modified during it's loop # Get all the sequences in the hierarchy. It will create them, if # they don't exist. sequences = [] From 409bf1b6e51c9ea6ab1fe6be1aca58dda8aa0bb4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 05:24:42 +0300 Subject: [PATCH 067/129] Rename `attr` into `instance` refactor. --- openpype/hosts/maya/plugins/create/create_review.py | 4 ++-- openpype/hosts/maya/plugins/publish/collect_review.py | 4 ++-- openpype/hosts/maya/plugins/publish/extract_playblast.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 83b7f34d82..44dc8a3158 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -35,8 +35,8 @@ class CreateReview(plugin.Creator): for key, value in animation_data.items(): data[key] = value - data["attrWidth"] = self.attrWidth - data["attrHeight"] = self.attrHeight + data["instanceHeight"] = self.attrWidth + data["instanceWidth"] = self.attrHeight data["isolate"] = self.isolate data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 83c4760535..fec1fbfa11 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -71,8 +71,8 @@ class CollectReview(pyblish.api.InstancePlugin): data['handles'] = instance.data.get('handles', None) data['step'] = instance.data['step'] data['fps'] = instance.data['fps'] - data['attrWidth'] = instance.data['attrWidth'] - data['attrHeight'] = instance.data['attrHeight'] + data['instanceHeight'] = instance.data['instanceHeight'] + data['instanceHeight'] = instance.data['instanceHeight'] data["isolate"] = instance.data["isolate"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 6f7dd5e16a..7f9875f564 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -59,8 +59,8 @@ class ExtractPlayblast(openpype.api.Extractor): # Set resolution variables from asset values asset_width = instance.data.get("resolutionWidth") asset_height = instance.data.get("resolutionHeight") - review_instance_width = instance.data.get("attrWidth") - review_instance_height = instance.data.get("attrHeight") + review_instance_width = instance.data.get("instanceWidth") + review_instance_height = instance.data.get("instanceHeight") preset['camera'] = camera # Tests if project resolution is set, From aca0c2a52b652f013922d71d27b55314c361ebe6 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 05:25:54 +0300 Subject: [PATCH 068/129] Fix thumbnail extractor to match playblast resolution. --- .../maya/plugins/publish/extract_thumbnail.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index c2cefc56f1..119ad10496 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -60,7 +60,32 @@ class ExtractThumbnail(openpype.api.Extractor): "overscan": 1.0, "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), } + capture_presets = capture_preset + # Set resolution variables from capture presets + width_preset = capture_presets["Resolution"]["width"] + height_preset = capture_presets["Resolution"]["height"] + # Set resolution variables from asset values + asset_width = instance.data.get("resolutionWidth") + asset_height = instance.data.get("resolutionHeight") + review_instance_width = instance.data.get("instanceWidth") + review_instance_height = instance.data.get("instanceHeight") + # Tests if project resolution is set, + # if it is a value other than zero, that value is + # used, if not then the asset resolution is + # used + if review_instance_width != 0: + preset['width'] = review_instance_width + elif width_preset == 0: + preset['width'] = asset_width + elif width_preset != 0: + preset['width'] = width_preset + if review_instance_height != 0: + preset['height'] = review_instance_height + elif height_preset == 0: + preset['height'] = asset_height + elif height_preset != 0: + preset['height'] = height_preset stagingDir = self.staging_dir(instance) filename = "{0}".format(instance.name) path = os.path.join(stagingDir, filename) From 0c674fcc61a636ebfc5e888e0f0f782dffa366f1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 28 Jun 2022 09:15:09 +0200 Subject: [PATCH 069/129] expand spacing of the drop zone --- openpype/widgets/attribute_defs/files_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/widgets/attribute_defs/files_widget.py b/openpype/widgets/attribute_defs/files_widget.py index 3135da6691..698a91a1a5 100644 --- a/openpype/widgets/attribute_defs/files_widget.py +++ b/openpype/widgets/attribute_defs/files_widget.py @@ -43,11 +43,11 @@ class DropEmpty(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addSpacing(10) + layout.addSpacing(20) layout.addWidget( drop_label_widget, 0, alignment=QtCore.Qt.AlignCenter ) - layout.addSpacing(10) + layout.addSpacing(30) layout.addStretch(1) layout.addWidget( items_label_widget, 0, alignment=QtCore.Qt.AlignCenter From 46bfa3122f1f6cfbc9f70b8ad65b68974c072dd6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 10:33:51 +0200 Subject: [PATCH 070/129] fix typo in typo --- openpype/lib/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 2730ba1f3c..18028c5f06 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -65,7 +65,7 @@ def range_from_frames(*args, **kwargs): @editorial_deprecated -def frames_to_seconds(*args, **kwargs): +def frames_to_secons(*args, **kwargs): from openpype.pipeline.editorial import frames_to_seconds return frames_to_seconds(*args, **kwargs) From 843d92484df1c19b53d95bac49ea44aeb8e2a784 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 10:42:48 +0200 Subject: [PATCH 071/129] added special deprecation warning error --- openpype/lib/editorial.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 18028c5f06..49220b4f15 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -1,7 +1,16 @@ +"""Code related to editorial utility functions was moved +to 'openpype.pipeline.editorial' please change your imports as soon as +possible. File will be probably removed in OpenPype 3.14.* +""" + import warnings import functools +class EditorialDeprecatedWarning(DeprecationWarning): + pass + + def editorial_deprecated(func): """Mark functions as deprecated. @@ -10,12 +19,13 @@ def editorial_deprecated(func): @functools.wraps(func) def new_func(*args, **kwargs): + warnings.simplefilter("always", EditorialDeprecatedWarning) warnings.warn( ( "Call to deprecated function '{}'." " Function was moved to 'openpype.pipeline.editorial'." ).format(func.__name__), - category=DeprecationWarning, + category=EditorialDeprecatedWarning, stacklevel=2 ) return func(*args, **kwargs) From f846ef45d473ba4849f4bb0ed73fc7f9fcb1a546 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 10:52:12 +0200 Subject: [PATCH 072/129] 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["_"] From 4450d3e7374ad1e158e8890c8ad0a16bb9845857 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 12:44:24 +0200 Subject: [PATCH 073/129] add parent_ids to possible query filters --- openpype/client/entities.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 9864fee469..28cd994254 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -146,6 +146,7 @@ def _get_assets( project_name, asset_ids=None, asset_names=None, + parent_ids=None, standard=True, archived=False, fields=None @@ -161,6 +162,7 @@ def _get_assets( project_name (str): Name of project where to look for queried entities. asset_ids (list[str|ObjectId]): Asset ids that should be found. asset_names (list[str]): Name assets that should be found. + parent_ids (list[str|ObjectId]): Parent asset ids. standard (bool): Query standart assets (type 'asset'). archived (bool): Query archived assets (type 'archived_asset'). fields (list[str]): Fields that should be returned. All fields are @@ -196,6 +198,12 @@ def _get_assets( return [] query_filter["name"] = {"$in": list(asset_names)} + if parent_ids is not None: + parent_ids = _convert_ids(parent_ids) + if not parent_ids: + return [] + query_filter["data.visualParent"] = {"$in": parent_ids} + conn = _get_project_connection(project_name) return conn.find(query_filter, _prepare_fields(fields)) @@ -205,6 +213,7 @@ def get_assets( project_name, asset_ids=None, asset_names=None, + parent_ids=None, archived=False, fields=None ): @@ -219,6 +228,7 @@ def get_assets( project_name (str): Name of project where to look for queried entities. asset_ids (list[str|ObjectId]): Asset ids that should be found. asset_names (list[str]): Name assets that should be found. + parent_ids (list[str|ObjectId]): Parent asset ids. archived (bool): Add also archived assets. fields (list[str]): Fields that should be returned. All fields are returned if 'None' is passed. @@ -229,7 +239,13 @@ def get_assets( """ return _get_assets( - project_name, asset_ids, asset_names, True, archived, fields + project_name, + asset_ids, + asset_names, + parent_ids, + True, + archived, + fields ) @@ -237,6 +253,7 @@ def get_archived_assets( project_name, asset_ids=None, asset_names=None, + parent_ids=None, fields=None ): """Archived assets for specified project by passed filters. @@ -250,6 +267,7 @@ def get_archived_assets( project_name (str): Name of project where to look for queried entities. asset_ids (list[str|ObjectId]): Asset ids that should be found. asset_names (list[str]): Name assets that should be found. + parent_ids (list[str|ObjectId]): Parent asset ids. fields (list[str]): Fields that should be returned. All fields are returned if 'None' is passed. @@ -259,7 +277,7 @@ def get_archived_assets( """ return _get_assets( - project_name, asset_ids, asset_names, False, True, fields + project_name, asset_ids, asset_names, parent_ids, False, True, fields ) From 35e5f7a3d256c799c18765a3d1e712e60a28ca9b Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 14:02:25 +0300 Subject: [PATCH 074/129] Expose excluded families in publisher schema. --- .../defaults/project_settings/maya.json | 3 +- .../schemas/schema_maya_publish.json | 35 +++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index cdd3a62d00..89aa7a66e6 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -204,7 +204,8 @@ "ValidateFrameRange": { "enabled": true, "optional": true, - "active": true + "active": true, + "exclude_families": [] }, "ValidateShaderName": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 41b681d893..84182973a1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -62,13 +62,36 @@ } ] }, - { - "type": "schema_template", - "name": "template_publish_plugin", - "template_data": [ + { + "type": "dict", + "collapsible": true, + "key": "ValidateFrameRange", + "label": "Validate Frame Range", + "checkbox_key": "enabled", + "children": [ { - "key": "ValidateFrameRange", - "label": "Validate Frame Range" + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "splitter" + }, + { + "key": "exclude_families", + "label": "Families", + "type": "list", + "object_type": "text" } ] }, From c7b5a6ac84cd8ec3ecbce11add48f7982458c770 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 14:02:39 +0300 Subject: [PATCH 075/129] Add additional exclude families. --- openpype/hosts/maya/plugins/publish/validate_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_frame_range.py b/openpype/hosts/maya/plugins/publish/validate_frame_range.py index 4415815d32..d07a8db957 100644 --- a/openpype/hosts/maya/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/maya/plugins/publish/validate_frame_range.py @@ -27,7 +27,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): "yeticache"] optional = True actions = [openpype.api.RepairAction] - exclude_families = ["model"] + exclude_families = ["model", "rig", "staticMesh"] def process(self, instance): context = instance.context From 809153b1449da7b6614a296ce19e95a446882f95 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Tue, 28 Jun 2022 14:42:11 +0300 Subject: [PATCH 076/129] Update openpype/settings/defaults/project_settings/maya.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move default families. Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 89aa7a66e6..9bebd92cb9 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -205,7 +205,7 @@ "enabled": true, "optional": true, "active": true, - "exclude_families": [] + "exclude_families": ["model", "rig", "staticMesh"] }, "ValidateShaderName": { "enabled": false, From 027cdf4f3a0dfc10b96555bf9920c3e9e3287cbe Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 14:49:31 +0300 Subject: [PATCH 077/129] Remove unnecessary defaults. --- openpype/hosts/maya/plugins/publish/validate_frame_range.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_frame_range.py b/openpype/hosts/maya/plugins/publish/validate_frame_range.py index d07a8db957..c51766379e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_frame_range.py +++ b/openpype/hosts/maya/plugins/publish/validate_frame_range.py @@ -27,7 +27,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): "yeticache"] optional = True actions = [openpype.api.RepairAction] - exclude_families = ["model", "rig", "staticMesh"] + exclude_families = [] def process(self, instance): context = instance.context From 449824ff479723b54ef2a5547cef1399a88c9ba6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 14:05:07 +0200 Subject: [PATCH 078/129] added aiohttps middlewares dependency --- poetry.lock | 17 +++++++++++++++++ pyproject.toml | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 47509f334e..7221e191ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -46,6 +46,19 @@ python-versions = ">=3.5" [package.dependencies] aiohttp = ">=3,<4" +[[package]] +name = "aiohttp-middlewares" +version = "2.0.0" +description = "Collection of useful middlewares for aiohttp applications." +category = "main" +optional = false +python-versions = ">=3.7,<4.0" + +[package.dependencies] +aiohttp = ">=3.8.1,<4.0.0" +async-timeout = ">=4.0.2,<5.0.0" +yarl = ">=1.5.1,<2.0.0" + [[package]] name = "aiosignal" version = "1.2.0" @@ -1783,6 +1796,10 @@ aiohttp-json-rpc = [ {file = "aiohttp-json-rpc-0.13.3.tar.gz", hash = "sha256:6237a104478c22c6ef96c7227a01d6832597b414e4b79a52d85593356a169e99"}, {file = "aiohttp_json_rpc-0.13.3-py3-none-any.whl", hash = "sha256:4fbd197aced61bd2df7ae3237ead7d3e08833c2ccf48b8581e1828c95ebee680"}, ] +aiohttp-middlewares = [ + {file = "aiohttp-middlewares-2.0.0.tar.gz", hash = "sha256:e08ba04dc0e8fe379aa5e9444a68485c275677ee1e18c55cbb855de0c3629502"}, + {file = "aiohttp_middlewares-2.0.0-py3-none-any.whl", hash = "sha256:29cf1513176b4013844711975ff520e26a8a5d8f9fefbbddb5e91224a86b043e"}, +] aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, diff --git a/pyproject.toml b/pyproject.toml index a159559763..09dfdf45cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ slack-sdk = "^3.6.0" requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" +aiohttp-middlewares = "^2.0.0" [tool.poetry.dev-dependencies] @@ -154,4 +155,4 @@ exclude = [ ignore = ["website", "docs", ".git"] reportMissingImports = true -reportMissingTypeStubs = false \ No newline at end of file +reportMissingTypeStubs = false From 81cb958470371a635e7bc038a5907e32b394f073 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 14:08:53 +0200 Subject: [PATCH 079/129] added cors middleware --- openpype/modules/webserver/cors_middleware.py | 284 ++++++++++++++++++ openpype/modules/webserver/server.py | 11 +- 2 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 openpype/modules/webserver/cors_middleware.py diff --git a/openpype/modules/webserver/cors_middleware.py b/openpype/modules/webserver/cors_middleware.py new file mode 100644 index 0000000000..0c47f9194e --- /dev/null +++ b/openpype/modules/webserver/cors_middleware.py @@ -0,0 +1,284 @@ +r""" +=============== +CORS Middleware +=============== +.. versionadded:: 0.2.0 +Dealing with CORS headers for aiohttp applications. +**IMPORTANT:** There is a `aiohttp-cors +`_ library, which handles CORS +headers by attaching additional handlers to aiohttp application for +OPTIONS (preflight) requests. In same time this CORS middleware mimics the +logic of `django-cors-headers `_, +where all handling done in the middleware without any additional handlers. This +approach allows aiohttp application to respond with CORS headers for OPTIONS or +wildcard handlers, which is not possible with ``aiohttp-cors`` due to +https://github.com/aio-libs/aiohttp-cors/issues/241 issue. +For detailed information about CORS (Cross Origin Resource Sharing) please +visit: +- `Wikipedia `_ +- Or `MDN `_ +Configuration +============= +**IMPORTANT:** By default, CORS middleware do not allow any origins to access +content from your aiohttp appliction. Which means, you need carefully check +possible options and provide custom values for your needs. +Usage +===== +.. code-block:: python + import re + from aiohttp import web + from aiohttp_middlewares import cors_middleware + from aiohttp_middlewares.cors import DEFAULT_ALLOW_HEADERS + # Unsecure configuration to allow all CORS requests + app = web.Application( + middlewares=[cors_middleware(allow_all=True)] + ) + # Allow CORS requests from URL http://localhost:3000 + app = web.Application( + middlewares=[ + cors_middleware(origins=["http://localhost:3000"]) + ] + ) + # Allow CORS requests from all localhost urls + app = web.Application( + middlewares=[ + cors_middleware( + origins=[re.compile(r"^https?\:\/\/localhost")] + ) + ] + ) + # Allow CORS requests from https://frontend.myapp.com as well + # as allow credentials + CORS_ALLOW_ORIGINS = ["https://frontend.myapp.com"] + app = web.Application( + middlewares=[ + cors_middleware( + origins=CORS_ALLOW_ORIGINS, + allow_credentials=True, + ) + ] + ) + # Allow CORS requests only for API urls + app = web.Application( + middelwares=[ + cors_middleware( + origins=CORS_ALLOW_ORIGINS, + urls=[re.compile(r"^\/api")], + ) + ] + ) + # Allow CORS requests for POST & PATCH methods, and for all + # default headers and `X-Client-UID` + app = web.Application( + middlewares=[ + cors_middleware( + origings=CORS_ALLOW_ORIGINS, + allow_methods=("POST", "PATCH"), + allow_headers=DEFAULT_ALLOW_HEADERS + + ("X-Client-UID",), + ) + ] + ) +""" + +import logging +import re +from typing import Pattern, Tuple + +from aiohttp import web + +from aiohttp_middlewares.annotations import ( + Handler, + Middleware, + StrCollection, + UrlCollection, +) +from aiohttp_middlewares.utils import match_path + + +ACCESS_CONTROL = "Access-Control" +ACCESS_CONTROL_ALLOW = f"{ACCESS_CONTROL}-Allow" +ACCESS_CONTROL_ALLOW_CREDENTIALS = f"{ACCESS_CONTROL_ALLOW}-Credentials" +ACCESS_CONTROL_ALLOW_HEADERS = f"{ACCESS_CONTROL_ALLOW}-Headers" +ACCESS_CONTROL_ALLOW_METHODS = f"{ACCESS_CONTROL_ALLOW}-Methods" +ACCESS_CONTROL_ALLOW_ORIGIN = f"{ACCESS_CONTROL_ALLOW}-Origin" +ACCESS_CONTROL_EXPOSE_HEADERS = f"{ACCESS_CONTROL}-Expose-Headers" +ACCESS_CONTROL_MAX_AGE = f"{ACCESS_CONTROL}-Max-Age" +ACCESS_CONTROL_REQUEST_METHOD = f"{ACCESS_CONTROL}-Request-Method" + +DEFAULT_ALLOW_HEADERS = ( + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-csrftoken", + "x-requested-with", +) +DEFAULT_ALLOW_METHODS = ("DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT") +DEFAULT_URLS: Tuple[Pattern[str]] = (re.compile(r".*"),) + +logger = logging.getLogger(__name__) + + +def cors_middleware( + *, + allow_all: bool = False, + origins: UrlCollection = None, + urls: UrlCollection = None, + expose_headers: StrCollection = None, + allow_headers: StrCollection = DEFAULT_ALLOW_HEADERS, + allow_methods: StrCollection = DEFAULT_ALLOW_METHODS, + allow_credentials: bool = False, + max_age: int = None, +) -> Middleware: + """Middleware to provide CORS headers for aiohttp applications. + :param allow_all: + When enabled, allow any Origin to access content from your aiohttp web + application. **Please be careful with enabling this option as it may + result in security issues for your application.** By default: ``False`` + :param origins: + Allow content access for given list of origins. Support supplying + strings for exact origin match or regex instances. By default: ``None`` + :param urls: + Allow contect access for given list of URLs in aiohttp application. + By default: *apply CORS headers for all URLs* + :param expose_headers: + List of headers to be exposed with every CORS request. By default: + ``None`` + :param allow_headers: + List of allowed headers. By default: + .. code-block:: python + ( + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-csrftoken", + "x-requested-with", + ) + :param allow_methods: + List of allowed methods. By default: + .. code-block:: python + ("DELETE", "GET", "OPTIONS", "PATCH", "POST", "PUT") + :param allow_credentials: + When enabled apply allow credentials header in response, which results + in sharing cookies on shared resources. **Please be careful with + allowing credentials for CORS requests.** By default: ``False`` + :param max_age: Access control max age in seconds. By default: ``None`` + """ + check_urls: UrlCollection = DEFAULT_URLS if urls is None else urls + + @web.middleware + async def middleware( + request: web.Request, handler: Handler + ) -> web.StreamResponse: + # Initial vars + request_method = request.method + request_path = request.rel_url.path + + # Is this an OPTIONS request + is_options_request = request_method == "OPTIONS" + + # Is this a preflight request + is_preflight_request = ( + is_options_request + and ACCESS_CONTROL_REQUEST_METHOD in request.headers + ) + + # Log extra data + log_extra = { + "is_preflight_request": is_preflight_request, + "method": request_method.lower(), + "path": request_path, + } + + # Check whether CORS should be enabled for given URL or not. By default + # CORS enabled for all URLs + if not match_items(check_urls, request_path): + logger.debug( + "Request should not be processed via CORS middleware", + extra=log_extra, + ) + return await handler(request) + + # If this is a preflight request - generate empty response + if is_preflight_request: + response = web.StreamResponse() + # Otherwise - call actual handler + else: + response = await handler(request) + + # Now check origin heaer + origin = request.headers.get("Origin") + # Empty origin - do nothing + if not origin: + logger.debug( + "Request does not have Origin header. CORS headers not " + "available for given requests", + extra=log_extra, + ) + return response + + # Set allow credentials header if necessary + if allow_credentials: + response.headers[ACCESS_CONTROL_ALLOW_CREDENTIALS] = "true" + + # Check whether current origin satisfies CORS policy + if not allow_all and not (origins and match_items(origins, origin)): + logger.debug( + "CORS headers not allowed for given Origin", extra=log_extra + ) + return response + + # Now start supplying CORS headers + # First one is Access-Control-Allow-Origin + if allow_all and not allow_credentials: + cors_origin = "*" + else: + cors_origin = origin + response.headers[ACCESS_CONTROL_ALLOW_ORIGIN] = cors_origin + + # Then Access-Control-Expose-Headers + if expose_headers: + response.headers[ACCESS_CONTROL_EXPOSE_HEADERS] = ", ".join( + expose_headers + ) + + # Now, if this is an options request, respond with extra Allow headers + if is_options_request: + response.headers[ACCESS_CONTROL_ALLOW_HEADERS] = ", ".join( + allow_headers + ) + response.headers[ACCESS_CONTROL_ALLOW_METHODS] = ", ".join( + allow_methods + ) + if max_age is not None: + response.headers[ACCESS_CONTROL_MAX_AGE] = str(max_age) + + # If this is preflight request - do not allow other middlewares to + # process this request + if is_preflight_request: + logger.debug( + "Provide CORS headers with empty response for preflight " + "request", + extra=log_extra, + ) + raise web.HTTPOk(text="", headers=response.headers) + + # Otherwise return normal response + logger.debug("Provide CORS headers for request", extra=log_extra) + return response + + return middleware + + + +def match_items(items: UrlCollection, value: str) -> bool: + """Go through all items and try to match item with given value.""" + return any(match_path(item, value) for item in items) diff --git a/openpype/modules/webserver/server.py b/openpype/modules/webserver/server.py index 83a29e074e..82b681f406 100644 --- a/openpype/modules/webserver/server.py +++ b/openpype/modules/webserver/server.py @@ -1,15 +1,18 @@ +import re import threading import asyncio from aiohttp import web from openpype.lib import PypeLogger +from .cors_middleware import cors_middleware log = PypeLogger.get_logger("WebServer") class WebServerManager: """Manger that care about web server thread.""" + def __init__(self, port=None, host=None): self.port = port or 8079 self.host = host or "localhost" @@ -18,7 +21,13 @@ class WebServerManager: self.handlers = {} self.on_stop_callbacks = [] - self.app = web.Application() + self.app = web.Application( + middlewares=[ + cors_middleware( + origins=[re.compile(r"^https?\:\/\/localhost")] + ) + ] + ) # add route with multiple methods for single "external app" From 4133adf5ed469f9e97a7c288d279f05cbba43ce1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 14:10:37 +0200 Subject: [PATCH 080/129] removed redundant line --- openpype/modules/webserver/cors_middleware.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/webserver/cors_middleware.py b/openpype/modules/webserver/cors_middleware.py index 0c47f9194e..f1cd7b04b3 100644 --- a/openpype/modules/webserver/cors_middleware.py +++ b/openpype/modules/webserver/cors_middleware.py @@ -278,7 +278,6 @@ def cors_middleware( return middleware - def match_items(items: UrlCollection, value: str) -> bool: """Go through all items and try to match item with given value.""" return any(match_path(item, value) for item in items) From b6312221b643f1c30a7e2dee67429564c8686ff1 Mon Sep 17 00:00:00 2001 From: kaa Date: Tue, 28 Jun 2022 14:36:45 +0200 Subject: [PATCH 081/129] Check for python with enabled pymalloc Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/blender/hooks/pre_pyside_install.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/blender/hooks/pre_pyside_install.py b/openpype/hosts/blender/hooks/pre_pyside_install.py index d0f2b3d417..e5f66d2a26 100644 --- a/openpype/hosts/blender/hooks/pre_pyside_install.py +++ b/openpype/hosts/blender/hooks/pre_pyside_install.py @@ -102,6 +102,9 @@ class InstallPySideToBlender(PreLaunchHook): python_executable = os.path.join(python_bin, "python.exe") else: python_executable = os.path.join(python_bin, python_version) + # Check for python with enabled 'pymalloc' + if not os.path.exists(python_executable): + python_executable += "m" if not os.path.exists(python_executable): self.log.warning( From db6a061733d5f15fbea57ac44850271abb097b41 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 28 Jun 2022 13:45:54 +0000 Subject: [PATCH 082/129] [Automated] Bump version --- CHANGELOG.md | 28 ++++++++++++++++------------ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d118bbff54..f4535a516c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,45 @@ # Changelog -## [3.12.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.12.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.11.1...HEAD) ### 📖 Documentation +- Fix typo in documentation: pyenv on mac [\#3417](https://github.com/pypeclub/OpenPype/pull/3417) - Linux: update OIIO package [\#3401](https://github.com/pypeclub/OpenPype/pull/3401) -- General: Add ability to change user value for templates [\#3366](https://github.com/pypeclub/OpenPype/pull/3366) -- Multiverse: expose some settings to GUI [\#3350](https://github.com/pypeclub/OpenPype/pull/3350) **🚀 Enhancements** +- Webserver: Added CORS middleware [\#3422](https://github.com/pypeclub/OpenPype/pull/3422) +- Attribute Defs UI: Files widget show what is allowed to drop in [\#3411](https://github.com/pypeclub/OpenPype/pull/3411) +- General: Add ability to change user value for templates [\#3366](https://github.com/pypeclub/OpenPype/pull/3366) - Hosts: More options for in-host callbacks [\#3357](https://github.com/pypeclub/OpenPype/pull/3357) +- Multiverse: expose some settings to GUI [\#3350](https://github.com/pypeclub/OpenPype/pull/3350) - Maya: Allow more data to be published along camera 🎥 [\#3304](https://github.com/pypeclub/OpenPype/pull/3304) **🐛 Bug fixes** +- NewPublisher: Fix subset name change on change of creator plugin [\#3420](https://github.com/pypeclub/OpenPype/pull/3420) +- Bug: fix invalid avalon import [\#3418](https://github.com/pypeclub/OpenPype/pull/3418) - Nuke: Fix keyword argument in query function [\#3414](https://github.com/pypeclub/OpenPype/pull/3414) +- Houdini: fix loading and updating vbd/bgeo sequences [\#3408](https://github.com/pypeclub/OpenPype/pull/3408) - Nuke: Collect representation files based on Write [\#3407](https://github.com/pypeclub/OpenPype/pull/3407) - General: Filter representations before integration start [\#3398](https://github.com/pypeclub/OpenPype/pull/3398) - Maya: look collector typo [\#3392](https://github.com/pypeclub/OpenPype/pull/3392) - TVPaint: Make sure exit code is set to not None [\#3382](https://github.com/pypeclub/OpenPype/pull/3382) - Maya: vray device aspect ratio fix [\#3381](https://github.com/pypeclub/OpenPype/pull/3381) +- Flame: bunch of publishing issues [\#3377](https://github.com/pypeclub/OpenPype/pull/3377) - Harmony: added unc path to zifile command in Harmony [\#3372](https://github.com/pypeclub/OpenPype/pull/3372) - Standalone: settings improvements [\#3355](https://github.com/pypeclub/OpenPype/pull/3355) - Nuke: Load full model hierarchy by default [\#3328](https://github.com/pypeclub/OpenPype/pull/3328) **🔀 Refactored code** +- Unreal: Use client query functions [\#3421](https://github.com/pypeclub/OpenPype/pull/3421) +- General: Move editorial lib to pipeline [\#3419](https://github.com/pypeclub/OpenPype/pull/3419) - Kitsu: renaming to plural func sync\_all\_projects [\#3397](https://github.com/pypeclub/OpenPype/pull/3397) +- Houdini: Use client query functions [\#3395](https://github.com/pypeclub/OpenPype/pull/3395) - Hiero: Use client query functions [\#3393](https://github.com/pypeclub/OpenPype/pull/3393) - Nuke: Use client query functions [\#3391](https://github.com/pypeclub/OpenPype/pull/3391) - Maya: Use client query functions [\#3385](https://github.com/pypeclub/OpenPype/pull/3385) @@ -43,6 +53,7 @@ **Merged pull requests:** +- Sync Queue: Added far future value for null values for dates [\#3371](https://github.com/pypeclub/OpenPype/pull/3371) - Maya - added support for single frame playblast review [\#3369](https://github.com/pypeclub/OpenPype/pull/3369) ## [3.11.1](https://github.com/pypeclub/OpenPype/tree/3.11.1) (2022-06-20) @@ -73,7 +84,6 @@ - nuke: adding extract thumbnail settings 3.10 [\#3347](https://github.com/pypeclub/OpenPype/pull/3347) - General: Fix last version function [\#3345](https://github.com/pypeclub/OpenPype/pull/3345) - Deadline: added OPENPYPE\_MONGO to filter [\#3336](https://github.com/pypeclub/OpenPype/pull/3336) -- Nuke: fixing farm publishing if review is disabled [\#3306](https://github.com/pypeclub/OpenPype/pull/3306) **🔀 Refactored code** @@ -97,17 +107,15 @@ - Ftrack: Delivery action can work on ReviewSessions [\#3307](https://github.com/pypeclub/OpenPype/pull/3307) - Maya: Look assigner UI improvements [\#3298](https://github.com/pypeclub/OpenPype/pull/3298) - Ftrack: Action to transfer values of hierarchical attributes [\#3284](https://github.com/pypeclub/OpenPype/pull/3284) -- Maya: better handling of legacy review subsets names [\#3269](https://github.com/pypeclub/OpenPype/pull/3269) -- General: Updated windows oiio tool [\#3268](https://github.com/pypeclub/OpenPype/pull/3268) -- Unreal: add support for skeletalMesh and staticMesh to loaders [\#3267](https://github.com/pypeclub/OpenPype/pull/3267) -- Maya: reference loaders could store placeholder in referenced url [\#3264](https://github.com/pypeclub/OpenPype/pull/3264) **🐛 Bug fixes** - General: Handle empty source key on instance [\#3342](https://github.com/pypeclub/OpenPype/pull/3342) +- General: Handle empty source key on instance [\#3341](https://github.com/pypeclub/OpenPype/pull/3341) - Houdini: Fix Houdini VDB manage update wrong file attribute name [\#3322](https://github.com/pypeclub/OpenPype/pull/3322) - Nuke: anatomy compatibility issue hacks [\#3321](https://github.com/pypeclub/OpenPype/pull/3321) - hiero: otio p3 compatibility issue - metadata on effect use update 3.11 [\#3314](https://github.com/pypeclub/OpenPype/pull/3314) +- Nuke: fixing farm publishing if review is disabled [\#3306](https://github.com/pypeclub/OpenPype/pull/3306) - General: Vendorized modules for Python 2 and update poetry lock [\#3305](https://github.com/pypeclub/OpenPype/pull/3305) - Fix - added local targets to install host [\#3303](https://github.com/pypeclub/OpenPype/pull/3303) - Settings: Add missing default settings for nuke gizmo [\#3301](https://github.com/pypeclub/OpenPype/pull/3301) @@ -115,9 +123,6 @@ - Maya: point cache publish handles Maya instances [\#3297](https://github.com/pypeclub/OpenPype/pull/3297) - Global: extract review slate issues [\#3286](https://github.com/pypeclub/OpenPype/pull/3286) - Webpublisher: return only active projects in ProjectsEndpoint [\#3281](https://github.com/pypeclub/OpenPype/pull/3281) -- Hiero: add support for task tags 3.10.x [\#3279](https://github.com/pypeclub/OpenPype/pull/3279) -- General: Fix Oiio tool path resolving [\#3278](https://github.com/pypeclub/OpenPype/pull/3278) -- Maya: Fix udim support for e.g. uppercase \ tag [\#3266](https://github.com/pypeclub/OpenPype/pull/3266) **🔀 Refactored code** @@ -127,7 +132,6 @@ **Merged pull requests:** - Maya: add pointcache family to gpu cache loader [\#3318](https://github.com/pypeclub/OpenPype/pull/3318) -- Maya look: skip empty file attributes [\#3274](https://github.com/pypeclub/OpenPype/pull/3274) ## [3.10.0](https://github.com/pypeclub/OpenPype/tree/3.10.0) (2022-05-26) diff --git a/openpype/version.py b/openpype/version.py index 02f928d83c..54808bfa81 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.0-nightly.2" +__version__ = "3.12.0-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 09dfdf45cd..df5630270d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.0-nightly.2" # OpenPype +version = "3.12.0-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From f92e0f8fa3c2a07aa9b5396be2fba4f50973535e Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 28 Jun 2022 13:55:09 +0000 Subject: [PATCH 083/129] [Automated] Release --- CHANGELOG.md | 9 ++++----- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4535a516c..003ca72a24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.12.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.11.1...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.11.1...3.12.0) ### 📖 Documentation @@ -29,7 +29,6 @@ - Maya: look collector typo [\#3392](https://github.com/pypeclub/OpenPype/pull/3392) - TVPaint: Make sure exit code is set to not None [\#3382](https://github.com/pypeclub/OpenPype/pull/3382) - Maya: vray device aspect ratio fix [\#3381](https://github.com/pypeclub/OpenPype/pull/3381) -- Flame: bunch of publishing issues [\#3377](https://github.com/pypeclub/OpenPype/pull/3377) - Harmony: added unc path to zifile command in Harmony [\#3372](https://github.com/pypeclub/OpenPype/pull/3372) - Standalone: settings improvements [\#3355](https://github.com/pypeclub/OpenPype/pull/3355) - Nuke: Load full model hierarchy by default [\#3328](https://github.com/pypeclub/OpenPype/pull/3328) @@ -74,6 +73,7 @@ **🐛 Bug fixes** +- Flame: bunch of publishing issues [\#3377](https://github.com/pypeclub/OpenPype/pull/3377) - Nuke: bake streams with slate on farm [\#3368](https://github.com/pypeclub/OpenPype/pull/3368) - Harmony: audio validator has wrong logic [\#3364](https://github.com/pypeclub/OpenPype/pull/3364) - Nuke: Fix missing variable in extract thumbnail [\#3363](https://github.com/pypeclub/OpenPype/pull/3363) @@ -84,6 +84,7 @@ - nuke: adding extract thumbnail settings 3.10 [\#3347](https://github.com/pypeclub/OpenPype/pull/3347) - General: Fix last version function [\#3345](https://github.com/pypeclub/OpenPype/pull/3345) - Deadline: added OPENPYPE\_MONGO to filter [\#3336](https://github.com/pypeclub/OpenPype/pull/3336) +- Nuke: fixing farm publishing if review is disabled [\#3306](https://github.com/pypeclub/OpenPype/pull/3306) **🔀 Refactored code** @@ -111,11 +112,9 @@ **🐛 Bug fixes** - General: Handle empty source key on instance [\#3342](https://github.com/pypeclub/OpenPype/pull/3342) -- General: Handle empty source key on instance [\#3341](https://github.com/pypeclub/OpenPype/pull/3341) - Houdini: Fix Houdini VDB manage update wrong file attribute name [\#3322](https://github.com/pypeclub/OpenPype/pull/3322) - Nuke: anatomy compatibility issue hacks [\#3321](https://github.com/pypeclub/OpenPype/pull/3321) - hiero: otio p3 compatibility issue - metadata on effect use update 3.11 [\#3314](https://github.com/pypeclub/OpenPype/pull/3314) -- Nuke: fixing farm publishing if review is disabled [\#3306](https://github.com/pypeclub/OpenPype/pull/3306) - General: Vendorized modules for Python 2 and update poetry lock [\#3305](https://github.com/pypeclub/OpenPype/pull/3305) - Fix - added local targets to install host [\#3303](https://github.com/pypeclub/OpenPype/pull/3303) - Settings: Add missing default settings for nuke gizmo [\#3301](https://github.com/pypeclub/OpenPype/pull/3301) diff --git a/openpype/version.py b/openpype/version.py index 54808bfa81..65e439ef33 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.0-nightly.3" +__version__ = "3.12.0" diff --git a/pyproject.toml b/pyproject.toml index df5630270d..bd5d3ad89d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.0-nightly.3" # OpenPype +version = "3.12.0" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From c69482df0a8a4b67952a908c6f4431524c039413 Mon Sep 17 00:00:00 2001 From: Kaa Maurice Date: Tue, 28 Jun 2022 17:06:13 +0200 Subject: [PATCH 084/129] Blender bugfix on open set fps --- openpype/hosts/blender/api/pipeline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index 93d81145bc..ea405b028e 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -93,7 +93,7 @@ def set_start_end_frames(): # Default scene settings frameStart = scene.frame_start frameEnd = scene.frame_end - fps = scene.render.fps + fps = scene.render.fps / scene.render.fps_base resolution_x = scene.render.resolution_x resolution_y = scene.render.resolution_y @@ -116,7 +116,8 @@ def set_start_end_frames(): scene.frame_start = frameStart scene.frame_end = frameEnd - scene.render.fps = fps + scene.render.fps = round(fps) + scene.render.fps_base = round(fps) / fps scene.render.resolution_x = resolution_x scene.render.resolution_y = resolution_y From e9edb1cb68bc65c161a5dfa1e4b4eeda322a0b23 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Tue, 28 Jun 2022 18:20:57 +0300 Subject: [PATCH 085/129] Adjust dimensions to grab correct values from asset. Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 7f9875f564..8d1c62d64c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -57,8 +57,9 @@ class ExtractPlayblast(openpype.api.Extractor): width_preset = capture_presets["Resolution"]["width"] height_preset = capture_presets["Resolution"]["height"] # Set resolution variables from asset values - asset_width = instance.data.get("resolutionWidth") - asset_height = instance.data.get("resolutionHeight") + asset_data = instance.data["assetEntity"]["data"] + asset_width = asset_data.get("width") + asset_height = asset_data.get("height") review_instance_width = instance.data.get("instanceWidth") review_instance_height = instance.data.get("instanceHeight") preset['camera'] = camera From be2f9a06aa611f1abb94c416416f5c5ad48c0ae6 Mon Sep 17 00:00:00 2001 From: "Allan I. A" <76656700+Allan-I@users.noreply.github.com> Date: Tue, 28 Jun 2022 18:42:44 +0300 Subject: [PATCH 086/129] Simplify testing of cases for resolution Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../maya/plugins/publish/extract_playblast.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 8d1c62d64c..0852053b8b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -69,18 +69,14 @@ class ExtractPlayblast(openpype.api.Extractor): # used, if not then the asset resolution is # used - if review_instance_width != 0: + if review_instance_width and review_instance_height: preset['width'] = review_instance_width - elif width_preset == 0: - preset['width'] = asset_width - elif width_preset != 0: - preset['width'] = width_preset - - if review_instance_height != 0: preset['height'] = review_instance_height - elif height_preset == 0: + elif asset_width and asset_height: + preset['width'] = asset_width preset['height'] = asset_height - elif height_preset != 0: + elif width_preset and height_preset: + preset['width'] = width_preset preset['height'] = height_preset preset['start_frame'] = start From c67308d03223656465525647c7254e65d61922f6 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 18:48:59 +0300 Subject: [PATCH 087/129] Variable rename. --- openpype/hosts/maya/plugins/create/create_review.py | 4 ++-- openpype/hosts/maya/plugins/publish/collect_review.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 44dc8a3158..bafc62e82c 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -35,8 +35,8 @@ class CreateReview(plugin.Creator): for key, value in animation_data.items(): data[key] = value - data["instanceHeight"] = self.attrWidth - data["instanceWidth"] = self.attrHeight + data["Width"] = self.attrWidth + data["Height"] = self.attrHeight data["isolate"] = self.isolate data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index fec1fbfa11..b0b5ef37e8 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -71,8 +71,8 @@ class CollectReview(pyblish.api.InstancePlugin): data['handles'] = instance.data.get('handles', None) data['step'] = instance.data['step'] data['fps'] = instance.data['fps'] - data['instanceHeight'] = instance.data['instanceHeight'] - data['instanceHeight'] = instance.data['instanceHeight'] + data['Width'] = instance.data['Width'] + data['Height'] = instance.data['Height'] data["isolate"] = instance.data["isolate"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) From bc18f8f755a771c27fcc9de452ba514d8039d555 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 19:04:10 +0300 Subject: [PATCH 088/129] Simplify logic in thumbnail extractor. --- .../maya/plugins/publish/extract_thumbnail.py | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 119ad10496..bc15327aa7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -65,26 +65,23 @@ class ExtractThumbnail(openpype.api.Extractor): width_preset = capture_presets["Resolution"]["width"] height_preset = capture_presets["Resolution"]["height"] # Set resolution variables from asset values - asset_width = instance.data.get("resolutionWidth") - asset_height = instance.data.get("resolutionHeight") - review_instance_width = instance.data.get("instanceWidth") - review_instance_height = instance.data.get("instanceHeight") + asset_data = instance.data["assetEntity"]["data"] + asset_width = asset_data.get("width") + asset_height = asset_data.get("height") + review_instance_width = instance.data.get("Width") + review_instance_height = instance.data.get("Height") # Tests if project resolution is set, # if it is a value other than zero, that value is # used, if not then the asset resolution is # used - if review_instance_width != 0: + if review_instance_width and review_instance_height: preset['width'] = review_instance_width - elif width_preset == 0: - preset['width'] = asset_width - elif width_preset != 0: - preset['width'] = width_preset - - if review_instance_height != 0: preset['height'] = review_instance_height - elif height_preset == 0: + elif asset_width and asset_height: + preset['width'] = asset_width preset['height'] = asset_height - elif height_preset != 0: + elif width_preset and height_preset: + preset['width'] = width_preset preset['height'] = height_preset stagingDir = self.staging_dir(instance) filename = "{0}".format(instance.name) From 4514ed8e18abf6dae25310d4f8acb86d3870b5f3 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 19:04:26 +0300 Subject: [PATCH 089/129] Change naming in interface. --- openpype/hosts/maya/plugins/create/create_review.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index bafc62e82c..9f6721762b 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -15,8 +15,8 @@ class CreateReview(plugin.Creator): keepImages = False isolate = False imagePlane = True - attrWidth = 0 - attrHeight = 0 + Width = 0 + Height = 0 transparency = [ "preset", "simple", @@ -35,8 +35,8 @@ class CreateReview(plugin.Creator): for key, value in animation_data.items(): data[key] = value - data["Width"] = self.attrWidth - data["Height"] = self.attrHeight + data["Width"] = self.Width + data["Height"] = self.Height data["isolate"] = self.isolate data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane From 92bf597224cba29ff532ff1b63015f1346639e28 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 19:15:36 +0300 Subject: [PATCH 090/129] Fix instance variable name. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0852053b8b..fbf4035505 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -60,8 +60,8 @@ class ExtractPlayblast(openpype.api.Extractor): asset_data = instance.data["assetEntity"]["data"] asset_width = asset_data.get("width") asset_height = asset_data.get("height") - review_instance_width = instance.data.get("instanceWidth") - review_instance_height = instance.data.get("instanceHeight") + review_instance_width = instance.data.get("Width") + review_instance_height = instance.data.get("Height") preset['camera'] = camera # Tests if project resolution is set, From a310f499740b0989762663e25b45bad09d766225 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 18:43:11 +0200 Subject: [PATCH 091/129] store write node to instance data --- openpype/hosts/nuke/plugins/publish/precollect_writes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 049958bd07..a97f34b370 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -35,6 +35,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): if node is None: return + instance.data["writeNode"] = node self.log.debug("checking instance: {}".format(instance)) # Determine defined file type From 7c48b61206ef38e593c84a4c4291941eb0923f1f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 18:43:30 +0200 Subject: [PATCH 092/129] add file to representation files when slate is rendered --- .../plugins/publish/extract_slate_frame.py | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py index e0c4bdb953..6d930d358d 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py +++ b/openpype/hosts/nuke/plugins/publish/extract_slate_frame.py @@ -4,6 +4,7 @@ import nuke import copy import pyblish.api +import six import openpype from openpype.hosts.nuke.api import ( @@ -12,7 +13,6 @@ from openpype.hosts.nuke.api import ( get_view_process_node ) - class ExtractSlateFrame(openpype.api.Extractor): """Extracts movie and thumbnail with baked in luts @@ -236,6 +236,48 @@ class ExtractSlateFrame(openpype.api.Extractor): int(slate_first_frame) ) + # Add file to representation files + # - get write node + write_node = instance.data["writeNode"] + # - evaluate filepaths for first frame and slate frame + first_filename = os.path.basename( + write_node["file"].evaluate(first_frame)) + slate_filename = os.path.basename( + write_node["file"].evaluate(slate_first_frame)) + + # Find matching representation based on first filename + matching_repre = None + is_sequence = None + for repre in instance.data["representations"]: + files = repre["files"] + if ( + not isinstance(files, six.string_types) + and first_filename in files + ): + matching_repre = repre + is_sequence = True + break + + elif files == first_filename: + matching_repre = repre + is_sequence = False + break + + if not matching_repre: + self.log.info(( + "Matching reresentaion was not found." + " Representation files were not filled with slate." + )) + return + + # Add frame to matching representation files + if not is_sequence: + matching_repre["files"] = [first_filename, slate_filename] + elif slate_filename not in matching_repre["files"]: + matching_repre["files"].insert(0, slate_filename) + + self.log.warning("Added slate frame to representation files") + def add_comment_slate_node(self, instance, node): comment = instance.context.data.get("comment") From 778f485a1813b267ee47c5147b45e66011507d1f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 29 Jun 2022 04:03:54 +0000 Subject: [PATCH 093/129] [Automated] Bump version --- CHANGELOG.md | 18 +++++++++++------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 003ca72a24..438a563391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,16 @@ # Changelog +## [3.12.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.0...HEAD) + +**🚀 Enhancements** + +- Blender: pre pyside install for all platforms [\#3400](https://github.com/pypeclub/OpenPype/pull/3400) + ## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.11.1...3.12.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.0-nightly.3...3.12.0) ### 📖 Documentation @@ -29,6 +37,7 @@ - Maya: look collector typo [\#3392](https://github.com/pypeclub/OpenPype/pull/3392) - TVPaint: Make sure exit code is set to not None [\#3382](https://github.com/pypeclub/OpenPype/pull/3382) - Maya: vray device aspect ratio fix [\#3381](https://github.com/pypeclub/OpenPype/pull/3381) +- Flame: bunch of publishing issues [\#3377](https://github.com/pypeclub/OpenPype/pull/3377) - Harmony: added unc path to zifile command in Harmony [\#3372](https://github.com/pypeclub/OpenPype/pull/3372) - Standalone: settings improvements [\#3355](https://github.com/pypeclub/OpenPype/pull/3355) - Nuke: Load full model hierarchy by default [\#3328](https://github.com/pypeclub/OpenPype/pull/3328) @@ -48,6 +57,7 @@ - AfterEffects: Use client query functions [\#3374](https://github.com/pypeclub/OpenPype/pull/3374) - TVPaint: Use client query functions [\#3340](https://github.com/pypeclub/OpenPype/pull/3340) - Ftrack: Use client query functions [\#3339](https://github.com/pypeclub/OpenPype/pull/3339) +- Webpublisher: Use client query functions [\#3333](https://github.com/pypeclub/OpenPype/pull/3333) - Standalone Publisher: Use client query functions [\#3330](https://github.com/pypeclub/OpenPype/pull/3330) **Merged pull requests:** @@ -73,7 +83,6 @@ **🐛 Bug fixes** -- Flame: bunch of publishing issues [\#3377](https://github.com/pypeclub/OpenPype/pull/3377) - Nuke: bake streams with slate on farm [\#3368](https://github.com/pypeclub/OpenPype/pull/3368) - Harmony: audio validator has wrong logic [\#3364](https://github.com/pypeclub/OpenPype/pull/3364) - Nuke: Fix missing variable in extract thumbnail [\#3363](https://github.com/pypeclub/OpenPype/pull/3363) @@ -86,10 +95,6 @@ - Deadline: added OPENPYPE\_MONGO to filter [\#3336](https://github.com/pypeclub/OpenPype/pull/3336) - Nuke: fixing farm publishing if review is disabled [\#3306](https://github.com/pypeclub/OpenPype/pull/3306) -**🔀 Refactored code** - -- Webpublisher: Use client query functions [\#3333](https://github.com/pypeclub/OpenPype/pull/3333) - ## [3.11.0](https://github.com/pypeclub/OpenPype/tree/3.11.0) (2022-06-17) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.11.0-nightly.4...3.11.0) @@ -121,7 +126,6 @@ - Maya: Fix swaped width and height in reviews [\#3300](https://github.com/pypeclub/OpenPype/pull/3300) - Maya: point cache publish handles Maya instances [\#3297](https://github.com/pypeclub/OpenPype/pull/3297) - Global: extract review slate issues [\#3286](https://github.com/pypeclub/OpenPype/pull/3286) -- Webpublisher: return only active projects in ProjectsEndpoint [\#3281](https://github.com/pypeclub/OpenPype/pull/3281) **🔀 Refactored code** diff --git a/openpype/version.py b/openpype/version.py index 65e439ef33..633b0b4f33 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.0" +__version__ = "3.12.1-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index bd5d3ad89d..26a7b4bf1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.0" # OpenPype +version = "3.12.1-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 6779d0a88afc917c9da5f865ba32fc4c68907e1d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Jun 2022 13:17:33 +0200 Subject: [PATCH 094/129] Draft implementation for VDB to Arnold loader --- .../maya/plugins/load/load_vdb_to_arnold.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py b/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py new file mode 100644 index 0000000000..89166c2dc8 --- /dev/null +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -0,0 +1,137 @@ +import os + +from openpype.api import get_project_settings +from openpype.pipeline import load + +# TODO aiVolume doesn't automatically set velocity fps correctly, set manual? + + +class LoadVDBtoArnold(load.LoaderPlugin): + """Load OpenVDB for Arnold in aiVolume""" + + families = ["vdbcache"] + representations = ["vdb"] + + label = "Load VDB to Arnold" + icon = "cloud" + color = "orange" + + def load(self, context, name, namespace, data): + + from maya import cmds + from openpype.hosts.maya.api.pipeline import containerise + from openpype.hosts.maya.api.lib import unique_namespace + + try: + family = context["representation"]["context"]["family"] + except ValueError: + family = "vdbcache" + + # Check if the plugin for arnold is available on the pc + try: + cmds.loadPlugin("mtoa", quiet=True) + except Exception as exc: + self.log.error("Encountered exception:\n%s" % exc) + return + + asset = context['asset'] + asset_name = asset["name"] + namespace = namespace or unique_namespace( + asset_name + "_", + prefix="_" if asset_name[0].isdigit() else "", + suffix="_", + ) + + # Root group + label = "{}:{}".format(namespace, name) + root = cmds.group(name=label, empty=True) + + settings = get_project_settings(os.environ['AVALON_PROJECT']) + colors = settings['maya']['load']['colors'] + + c = colors.get(family) + if c is not None: + cmds.setAttr(root + ".useOutlinerColor", 1) + cmds.setAttr(root + ".outlinerColor", + (float(c[0]) / 255), + (float(c[1]) / 255), + (float(c[2]) / 255) + ) + + # Create VRayVolumeGrid + grid_node = cmds.createNode("aiVolume", + name="{}Shape".format(root), + parent=root) + + self._set_path(grid_node, + path=self.fname, + representation=context["representation"]) + + # Lock the shape node so the user can't delete the transform/shape + # as if it was referenced + cmds.lockNode(grid_node, lock=True) + + nodes = [root, grid_node] + self[:] = nodes + + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__) + + def update(self, container, representation): + + from maya import cmds + + path = api.get_representation_path(representation) + + # Find VRayVolumeGrid + members = cmds.sets(container['objectName'], query=True) + grid_nodes = cmds.ls(members, type="aiVolume", long=True) + assert len(grid_nodes) == 1, "This is a bug" + + # Update the VRayVolumeGrid + self._set_path(grid_nodes[0], path=path, representation=representation) + + # Update container representation + cmds.setAttr(container["objectName"] + ".representation", + str(representation["_id"]), + type="string") + + def switch(self, container, representation): + self.update(container, representation) + + def remove(self, container): + + from maya import cmds + + # Get all members of the avalon container, ensure they are unlocked + # and delete everything + members = cmds.sets(container['objectName'], query=True) + cmds.lockNode(members, lock=False) + cmds.delete([container['objectName']] + members) + + # Clean up the namespace + try: + cmds.namespace(removeNamespace=container['namespace'], + deleteNamespaceContent=True) + except RuntimeError: + pass + + @staticmethod + def _set_path(grid_node, + path, + representation): + """Apply the settings for the VDB path to the aiVolume node""" + from maya import cmds + + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) + + is_sequence = bool(representation["context"].get("frame")) + cmds.setAttr(grid_node + ".useFrameExtension", is_sequence) + + # Set file path + cmds.setAttr(grid_node + ".filename", path, type="string") From 251a54581c7c910bac425b24af06052dae46ea25 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Jun 2022 13:23:11 +0200 Subject: [PATCH 095/129] Fix missing import --- openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py b/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py index 89166c2dc8..d458c5abda 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -1,8 +1,10 @@ import os from openpype.api import get_project_settings -from openpype.pipeline import load - +from openpype.pipeline import ( + load, + get_representation_path +) # TODO aiVolume doesn't automatically set velocity fps correctly, set manual? @@ -85,7 +87,7 @@ class LoadVDBtoArnold(load.LoaderPlugin): from maya import cmds - path = api.get_representation_path(representation) + path = get_representation_path(representation) # Find VRayVolumeGrid members = cmds.sets(container['objectName'], query=True) From 866bbf0487b5339b7f0f4bafef548b04aca39f67 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 14:04:30 +0200 Subject: [PATCH 096/129] anatomy was moved to openpype pipeline --- openpype/lib/anatomy.py | 1263 +--------------------------------- openpype/pipeline/anatomy.py | 1260 +++++++++++++++++++++++++++++++++ 2 files changed, 1263 insertions(+), 1260 deletions(-) create mode 100644 openpype/pipeline/anatomy.py diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py index 3fbc05ee88..b62b207ade 100644 --- a/openpype/lib/anatomy.py +++ b/openpype/lib/anatomy.py @@ -1,1260 +1,3 @@ -import os -import re -import copy -import platform -import collections -import numbers - -from openpype.settings.lib import ( - get_default_anatomy_settings, - get_anatomy_settings -) -from .path_templates import ( - TemplateUnsolved, - TemplateResult, - TemplatesDict, - FormatObject, -) -from .log import PypeLogger - -log = PypeLogger().get_logger(__name__) - -try: - StringType = basestring -except NameError: - StringType = str - - -class ProjectNotSet(Exception): - """Exception raised when is created Anatomy without project name.""" - - -class RootCombinationError(Exception): - """This exception is raised when templates has combined root types.""" - - def __init__(self, roots): - joined_roots = ", ".join( - ["\"{}\"".format(_root) for _root in roots] - ) - # TODO better error message - msg = ( - "Combination of root with and" - " without root name in AnatomyTemplates. {}" - ).format(joined_roots) - - super(RootCombinationError, self).__init__(msg) - - -class Anatomy: - """Anatomy module helps to keep project settings. - - Wraps key project specifications, AnatomyTemplates and Roots. - - Args: - project_name (str): Project name to look on overrides. - """ - - root_key_regex = re.compile(r"{(root?[^}]+)}") - root_name_regex = re.compile(r"root\[([^]]+)\]") - - def __init__(self, project_name=None, site_name=None): - if not project_name: - project_name = os.environ.get("AVALON_PROJECT") - - if not project_name: - raise ProjectNotSet(( - "Implementation bug: Project name is not set. Anatomy requires" - " to load data for specific project." - )) - - self.project_name = project_name - - self._data = self._prepare_anatomy_data( - get_anatomy_settings(project_name, site_name) - ) - self._site_name = site_name - self._templates_obj = AnatomyTemplates(self) - self._roots_obj = Roots(self) - - # Anatomy used as dictionary - # - implemented only getters returning copy - def __getitem__(self, key): - return copy.deepcopy(self._data[key]) - - def get(self, key, default=None): - return copy.deepcopy(self._data).get(key, default) - - def keys(self): - return copy.deepcopy(self._data).keys() - - def values(self): - return copy.deepcopy(self._data).values() - - def items(self): - return copy.deepcopy(self._data).items() - - @staticmethod - def default_data(): - """Default project anatomy data. - - Always return fresh loaded data. May be used as data for new project. - - Not used inside Anatomy itself. - """ - return get_default_anatomy_settings(clear_metadata=False) - - @staticmethod - def _prepare_anatomy_data(anatomy_data): - """Prepare anatomy data for further processing. - - Method added to replace `{task}` with `{task[name]}` in templates. - """ - templates_data = anatomy_data.get("templates") - if templates_data: - # Replace `{task}` with `{task[name]}` in templates - value_queue = collections.deque() - value_queue.append(templates_data) - while value_queue: - item = value_queue.popleft() - if not isinstance(item, dict): - continue - - for key in tuple(item.keys()): - value = item[key] - if isinstance(value, dict): - value_queue.append(value) - - elif isinstance(value, StringType): - item[key] = value.replace("{task}", "{task[name]}") - return anatomy_data - - def reset(self): - """Reset values of cached data in templates and roots objects.""" - self._data = self._prepare_anatomy_data( - get_anatomy_settings(self.project_name, self._site_name) - ) - self.templates_obj.reset() - self.roots_obj.reset() - - @property - def templates(self): - """Wrap property `templates` of Anatomy's AnatomyTemplates instance.""" - return self._templates_obj.templates - - @property - def templates_obj(self): - """Return `AnatomyTemplates` object of current Anatomy instance.""" - return self._templates_obj - - def format(self, *args, **kwargs): - """Wrap `format` method of Anatomy's `templates_obj`.""" - return self._templates_obj.format(*args, **kwargs) - - def format_all(self, *args, **kwargs): - """Wrap `format_all` method of Anatomy's `templates_obj`.""" - return self._templates_obj.format_all(*args, **kwargs) - - @property - def roots(self): - """Wrap `roots` property of Anatomy's `roots_obj`.""" - return self._roots_obj.roots - - @property - def roots_obj(self): - """Return `Roots` object of current Anatomy instance.""" - return self._roots_obj - - def root_environments(self): - """Return OPENPYPE_ROOT_* environments for current project in dict.""" - return self._roots_obj.root_environments() - - def root_environmets_fill_data(self, template=None): - """Environment variable values in dictionary for rootless path. - - Args: - template (str): Template for environment variable key fill. - By default is set to `"${}"`. - """ - return self.roots_obj.root_environmets_fill_data(template) - - def find_root_template_from_path(self, *args, **kwargs): - """Wrapper for Roots `find_root_template_from_path`.""" - return self.roots_obj.find_root_template_from_path(*args, **kwargs) - - def path_remapper(self, *args, **kwargs): - """Wrapper for Roots `path_remapper`.""" - return self.roots_obj.path_remapper(*args, **kwargs) - - def all_root_paths(self): - """Wrapper for Roots `all_root_paths`.""" - return self.roots_obj.all_root_paths() - - def set_root_environments(self): - """Set OPENPYPE_ROOT_* environments for current project.""" - self._roots_obj.set_root_environments() - - def root_names(self): - """Return root names for current project.""" - return self.root_names_from_templates(self.templates) - - def _root_keys_from_templates(self, data): - """Extract root key from templates in data. - - Args: - data (dict): Data that may contain templates as string. - - Return: - set: Set of all root names from templates as strings. - - Output example: `{"root[work]", "root[publish]"}` - """ - - output = set() - if isinstance(data, dict): - for value in data.values(): - for root in self._root_keys_from_templates(value): - output.add(root) - - elif isinstance(data, str): - for group in re.findall(self.root_key_regex, data): - output.add(group) - - return output - - def root_value_for_template(self, template): - """Returns value of root key from template.""" - root_templates = [] - for group in re.findall(self.root_key_regex, template): - root_templates.append("{" + group + "}") - - if not root_templates: - return None - - return root_templates[0].format(**{"root": self.roots}) - - def root_names_from_templates(self, templates): - """Extract root names form anatomy templates. - - Returns None if values in templates contain only "{root}". - Empty list is returned if there is no "root" in templates. - Else returns all root names from templates in list. - - RootCombinationError is raised when templates contain both root types, - basic "{root}" and with root name specification "{root[work]}". - - Args: - templates (dict): Anatomy templates where roots are not filled. - - Return: - list/None: List of all root names from templates as strings when - multiroot setup is used, otherwise None is returned. - """ - roots = list(self._root_keys_from_templates(templates)) - # Return empty list if no roots found in templates - if not roots: - return roots - - # Raise exception when root keys have roots with and without root name. - # Invalid output example: ["root", "root[project]", "root[render]"] - if len(roots) > 1 and "root" in roots: - raise RootCombinationError(roots) - - # Return None if "root" without root name in templates - if len(roots) == 1 and roots[0] == "root": - return None - - names = set() - for root in roots: - for group in re.findall(self.root_name_regex, root): - names.add(group) - return list(names) - - def fill_root(self, template_path): - """Fill template path where is only "root" key unfilled. - - Args: - template_path (str): Path with "root" key in. - Example path: "{root}/projects/MyProject/Shot01/Lighting/..." - - Return: - str: formatted path - """ - # NOTE does not care if there are different keys than "root" - return template_path.format(**{"root": self.roots}) - - @classmethod - def fill_root_with_path(cls, rootless_path, root_path): - """Fill path without filled "root" key with passed path. - - This is helper to fill root with different directory path than anatomy - has defined no matter if is single or multiroot. - - Output path is same as input path if `rootless_path` does not contain - unfilled root key. - - Args: - rootless_path (str): Path without filled "root" key. Example: - "{root[work]}/MyProject/..." - root_path (str): What should replace root key in `rootless_path`. - - Returns: - str: Path with filled root. - """ - output = str(rootless_path) - for group in re.findall(cls.root_key_regex, rootless_path): - replacement = "{" + group + "}" - output = output.replace(replacement, root_path) - - return output - - def replace_root_with_env_key(self, filepath, template=None): - """Replace root of path with environment key. - - # Example: - ## Project with roots: - ``` - { - "nas": { - "windows": P:/projects", - ... - } - ... - } - ``` - - ## Entered filepath - "P:/projects/project/asset/task/animation_v001.ma" - - ## Entered template - "<{}>" - - ## Output - "/project/asset/task/animation_v001.ma" - - Args: - filepath (str): Full file path where root should be replaced. - template (str): Optional template for environment key. Must - have one index format key. - Default value if not entered: "${}" - - Returns: - str: Path where root is replaced with environment root key. - - Raise: - ValueError: When project's roots were not found in entered path. - """ - success, rootless_path = self.find_root_template_from_path(filepath) - if not success: - raise ValueError( - "{}: Project's roots were not found in path: {}".format( - self.project_name, filepath - ) - ) - - data = self.root_environmets_fill_data(template) - return rootless_path.format(**data) - - -class AnatomyTemplateUnsolved(TemplateUnsolved): - """Exception for unsolved template when strict is set to True.""" - - msg = "Anatomy template \"{0}\" is unsolved.{1}{2}" - - -class AnatomyTemplateResult(TemplateResult): - rootless = None - - def __new__(cls, result, rootless_path): - new_obj = super(AnatomyTemplateResult, cls).__new__( - cls, - str(result), - result.template, - result.solved, - result.used_values, - result.missing_keys, - result.invalid_types - ) - new_obj.rootless = rootless_path - return new_obj - - def validate(self): - if not self.solved: - raise AnatomyTemplateUnsolved( - self.template, - self.missing_keys, - self.invalid_types - ) - - -class AnatomyTemplates(TemplatesDict): - inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") - inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") - - def __init__(self, anatomy): - super(AnatomyTemplates, self).__init__() - self.anatomy = anatomy - self.loaded_project = None - - def __getitem__(self, key): - return self.templates[key] - - def get(self, key, default=None): - return self.templates.get(key, default) - - def reset(self): - self._raw_templates = None - self._templates = None - self._objected_templates = None - - @property - def project_name(self): - return self.anatomy.project_name - - @property - def roots(self): - return self.anatomy.roots - - @property - def templates(self): - self._validate_discovery() - return self._templates - - @property - def objected_templates(self): - self._validate_discovery() - return self._objected_templates - - def _validate_discovery(self): - if self.project_name != self.loaded_project: - self.reset() - - if self._templates is None: - self._discover() - self.loaded_project = self.project_name - - def _format_value(self, value, data): - if isinstance(value, RootItem): - return self._solve_dict(value, data) - - result = super(AnatomyTemplates, self)._format_value(value, data) - if isinstance(result, TemplateResult): - rootless_path = self._rootless_path(result, data) - result = AnatomyTemplateResult(result, rootless_path) - return result - - def set_templates(self, templates): - if not templates: - self.reset() - return - - self._raw_templates = copy.deepcopy(templates) - templates = copy.deepcopy(templates) - v_queue = collections.deque() - v_queue.append(templates) - while v_queue: - item = v_queue.popleft() - if not isinstance(item, dict): - continue - - for key in tuple(item.keys()): - value = item[key] - if isinstance(value, dict): - v_queue.append(value) - - elif ( - isinstance(value, StringType) - and "{task}" in value - ): - item[key] = value.replace("{task}", "{task[name]}") - - solved_templates = self.solve_template_inner_links(templates) - self._templates = solved_templates - self._objected_templates = self.create_ojected_templates( - solved_templates - ) - - def default_templates(self): - """Return default templates data with solved inner keys.""" - return self.solve_template_inner_links( - self.anatomy["templates"] - ) - - def _discover(self): - """ Loads anatomy templates from yaml. - Default templates are loaded if project is not set or project does - not have set it's own. - TODO: create templates if not exist. - - Returns: - TemplatesResultDict: Contain templates data for current project of - default templates. - """ - - if self.project_name is None: - # QUESTION create project specific if not found? - raise AssertionError(( - "Project \"{0}\" does not have his own templates." - " Trying to use default." - ).format(self.project_name)) - - self.set_templates(self.anatomy["templates"]) - - @classmethod - def replace_inner_keys(cls, matches, value, key_values, key): - """Replacement of inner keys in template values.""" - for match in matches: - anatomy_sub_keys = ( - cls.inner_key_name_pattern.findall(match) - ) - if key in anatomy_sub_keys: - raise ValueError(( - "Unsolvable recursion in inner keys, " - "key: \"{}\" is in his own value." - " Can't determine source, please check Anatomy templates." - ).format(key)) - - for anatomy_sub_key in anatomy_sub_keys: - replace_value = key_values.get(anatomy_sub_key) - if replace_value is None: - raise KeyError(( - "Anatomy templates can't be filled." - " Anatomy key `{0}` has" - " invalid inner key `{1}`." - ).format(key, anatomy_sub_key)) - - valid = isinstance(replace_value, (numbers.Number, StringType)) - if not valid: - raise ValueError(( - "Anatomy templates can't be filled." - " Anatomy key `{0}` has" - " invalid inner key `{1}`" - " with value `{2}`." - ).format(key, anatomy_sub_key, str(replace_value))) - - value = value.replace(match, str(replace_value)) - - return value - - @classmethod - def prepare_inner_keys(cls, key_values): - """Check values of inner keys. - - Check if inner key exist in template group and has valid value. - It is also required to avoid infinite loop with unsolvable recursion - when first inner key's value refers to second inner key's value where - first is used. - """ - keys_to_solve = set(key_values.keys()) - while True: - found = False - for key in tuple(keys_to_solve): - value = key_values[key] - - if isinstance(value, StringType): - matches = cls.inner_key_pattern.findall(value) - if not matches: - keys_to_solve.remove(key) - continue - - found = True - key_values[key] = cls.replace_inner_keys( - matches, value, key_values, key - ) - continue - - elif not isinstance(value, dict): - keys_to_solve.remove(key) - continue - - subdict_found = False - for _key, _value in tuple(value.items()): - matches = cls.inner_key_pattern.findall(_value) - if not matches: - continue - - subdict_found = True - found = True - key_values[key][_key] = cls.replace_inner_keys( - matches, _value, key_values, - "{}.{}".format(key, _key) - ) - - if not subdict_found: - keys_to_solve.remove(key) - - if not found: - break - - return key_values - - @classmethod - def solve_template_inner_links(cls, templates): - """Solve templates inner keys identified by "{@*}". - - Process is split into 2 parts. - First is collecting all global keys (keys in top hierarchy where value - is not dictionary). All global keys are set for all group keys (keys - in top hierarchy where value is dictionary). Value of a key is not - overridden in group if already contain value for the key. - - In second part all keys with "at" symbol in value are replaced with - value of the key afterward "at" symbol from the group. - - Args: - templates (dict): Raw templates data. - - Example: - templates:: - key_1: "value_1", - key_2: "{@key_1}/{filling_key}" - - group_1: - key_3: "value_3/{@key_2}" - - group_2: - key_2": "value_2" - key_4": "value_4/{@key_2}" - - output:: - key_1: "value_1" - key_2: "value_1/{filling_key}" - - group_1: { - key_1: "value_1" - key_2: "value_1/{filling_key}" - key_3: "value_3/value_1/{filling_key}" - - group_2: { - key_1: "value_1" - key_2: "value_2" - key_4: "value_3/value_2" - """ - default_key_values = templates.pop("defaults", {}) - for key, value in tuple(templates.items()): - if isinstance(value, dict): - continue - default_key_values[key] = templates.pop(key) - - # Pop "others" key before before expected keys are processed - other_templates = templates.pop("others") or {} - - keys_by_subkey = {} - for sub_key, sub_value in templates.items(): - key_values = {} - key_values.update(default_key_values) - key_values.update(sub_value) - keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - - for sub_key, sub_value in other_templates.items(): - if sub_key in keys_by_subkey: - log.warning(( - "Key \"{}\" is duplicated in others. Skipping." - ).format(sub_key)) - continue - - key_values = {} - key_values.update(default_key_values) - key_values.update(sub_value) - keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - - default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values) - - for key, value in default_keys_by_subkeys.items(): - keys_by_subkey[key] = value - - return keys_by_subkey - - def _dict_to_subkeys_list(self, subdict, pre_keys=None): - if pre_keys is None: - pre_keys = [] - output = [] - for key in subdict: - value = subdict[key] - result = list(pre_keys) - result.append(key) - if isinstance(value, dict): - for item in self._dict_to_subkeys_list(value, result): - output.append(item) - else: - output.append(result) - return output - - def _keys_to_dicts(self, key_list, value): - if not key_list: - return None - if len(key_list) == 1: - return {key_list[0]: value} - return {key_list[0]: self._keys_to_dicts(key_list[1:], value)} - - def _rootless_path(self, result, final_data): - used_values = result.used_values - missing_keys = result.missing_keys - template = result.template - invalid_types = result.invalid_types - if ( - "root" not in used_values - or "root" in missing_keys - or "{root" not in template - ): - return - - for invalid_type in invalid_types: - if "root" in invalid_type: - return - - root_keys = self._dict_to_subkeys_list({"root": used_values["root"]}) - if not root_keys: - return - - output = str(result) - for used_root_keys in root_keys: - if not used_root_keys: - continue - - used_value = used_values - root_key = None - for key in used_root_keys: - used_value = used_value[key] - if root_key is None: - root_key = key - else: - root_key += "[{}]".format(key) - - root_key = "{" + root_key + "}" - output = output.replace(str(used_value), root_key) - - return output - - def format(self, data, strict=True): - copy_data = copy.deepcopy(data) - roots = self.roots - if roots: - copy_data["root"] = roots - result = super(AnatomyTemplates, self).format(copy_data) - result.strict = strict - return result - - def format_all(self, in_data, only_keys=True): - """ Solves templates based on entered data. - - Args: - data (dict): Containing keys to be filled into template. - - Returns: - TemplatesResultDict: Output `TemplateResult` have `strict` - attribute set to False so accessing unfilled keys in templates - won't raise any exceptions. - """ - return self.format(in_data, strict=False) - - -class RootItem(FormatObject): - """Represents one item or roots. - - Holds raw data of root item specification. Raw data contain value - for each platform, but current platform value is used when object - is used for formatting of template. - - Args: - root_raw_data (dict): Dictionary containing root values by platform - names. ["windows", "linux" and "darwin"] - name (str, optional): Root name which is representing. Used with - multi root setup otherwise None value is expected. - parent_keys (list, optional): All dictionary parent keys. Values of - `parent_keys` are used for get full key which RootItem is - representing. Used for replacing root value in path with - formattable key. e.g. parent_keys == ["work"] -> {root[work]} - parent (object, optional): It is expected to be `Roots` object. - Value of `parent` won't affect code logic much. - """ - - def __init__( - self, root_raw_data, name=None, parent_keys=None, parent=None - ): - lowered_platform_keys = {} - for key, value in root_raw_data.items(): - lowered_platform_keys[key.lower()] = value - self.raw_data = lowered_platform_keys - self.cleaned_data = self._clean_roots(lowered_platform_keys) - self.name = name - self.parent_keys = parent_keys or [] - self.parent = parent - - self.available_platforms = list(lowered_platform_keys.keys()) - self.value = lowered_platform_keys.get(platform.system().lower()) - self.clean_value = self.clean_root(self.value) - - def __format__(self, *args, **kwargs): - return self.value.__format__(*args, **kwargs) - - def __str__(self): - return str(self.value) - - def __repr__(self): - return self.__str__() - - def __getitem__(self, key): - if isinstance(key, numbers.Number): - return self.value[key] - - additional_info = "" - if self.parent and self.parent.project_name: - additional_info += " for project \"{}\"".format( - self.parent.project_name - ) - - raise AssertionError( - "Root key \"{}\" is missing{}.".format( - key, additional_info - ) - ) - - def full_key(self): - """Full key value for dictionary formatting in template. - - Returns: - str: Return full replacement key for formatting. This helps when - multiple roots are set. In that case e.g. `"root[work]"` is - returned. - """ - if not self.name: - return "root" - - joined_parent_keys = "".join( - ["[{}]".format(key) for key in self.parent_keys] - ) - return "root{}".format(joined_parent_keys) - - def clean_path(self, path): - """Just replace backslashes with forward slashes.""" - return str(path).replace("\\", "/") - - def clean_root(self, root): - """Makes sure root value does not end with slash.""" - if root: - root = self.clean_path(root) - while root.endswith("/"): - root = root[:-1] - return root - - def _clean_roots(self, raw_data): - """Clean all values of raw root item values.""" - cleaned = {} - for key, value in raw_data.items(): - cleaned[key] = self.clean_root(value) - return cleaned - - def path_remapper(self, path, dst_platform=None, src_platform=None): - """Remap path for specific platform. - - Args: - path (str): Source path which need to be remapped. - dst_platform (str, optional): Specify destination platform - for which remapping should happen. - src_platform (str, optional): Specify source platform. This is - recommended to not use and keep unset until you really want - to use specific platform. - roots (dict/RootItem/None, optional): It is possible to remap - path with different roots then instance where method was - called has. - - Returns: - str/None: When path does not contain known root then - None is returned else returns remapped path with "{root}" - or "{root[]}". - """ - cleaned_path = self.clean_path(path) - if dst_platform: - dst_root_clean = self.cleaned_data.get(dst_platform) - if not dst_root_clean: - key_part = "" - full_key = self.full_key() - if full_key != "root": - key_part += "\"{}\" ".format(full_key) - - log.warning( - "Root {}miss platform \"{}\" definition.".format( - key_part, dst_platform - ) - ) - return None - - if cleaned_path.startswith(dst_root_clean): - return cleaned_path - - if src_platform: - src_root_clean = self.cleaned_data.get(src_platform) - if src_root_clean is None: - log.warning( - "Root \"{}\" miss platform \"{}\" definition.".format( - self.full_key(), src_platform - ) - ) - return None - - if not cleaned_path.startswith(src_root_clean): - return None - - subpath = cleaned_path[len(src_root_clean):] - if dst_platform: - # `dst_root_clean` is used from upper condition - return dst_root_clean + subpath - return self.clean_value + subpath - - result, template = self.find_root_template_from_path(path) - if not result: - return None - - def parent_dict(keys, value): - if not keys: - return value - - key = keys.pop(0) - return {key: parent_dict(keys, value)} - - if dst_platform: - format_value = parent_dict(list(self.parent_keys), dst_root_clean) - else: - format_value = parent_dict(list(self.parent_keys), self.value) - - return template.format(**{"root": format_value}) - - def find_root_template_from_path(self, path): - """Replaces known root value with formattable key in path. - - All platform values are checked for this replacement. - - Args: - path (str): Path where root value should be found. - - Returns: - tuple: Tuple contain 2 values: `success` (bool) and `path` (str). - When success it True then path should contain replaced root - value with formattable key. - - Example: - When input path is:: - "C:/windows/path/root/projects/my_project/file.ext" - - And raw data of item looks like:: - { - "windows": "C:/windows/path/root", - "linux": "/mount/root" - } - - Output will be:: - (True, "{root}/projects/my_project/file.ext") - - If any of raw data value wouldn't match path's root output is:: - (False, "C:/windows/path/root/projects/my_project/file.ext") - """ - result = False - output = str(path) - - root_paths = list(self.cleaned_data.values()) - mod_path = self.clean_path(path) - for root_path in root_paths: - # Skip empty paths - if not root_path: - continue - - if mod_path.startswith(root_path): - result = True - replacement = "{" + self.full_key() + "}" - output = replacement + mod_path[len(root_path):] - break - - return (result, output) - - -class Roots: - """Object which should be used for formatting "root" key in templates. - - Args: - anatomy Anatomy: Anatomy object created for a specific project. - """ - - env_prefix = "OPENPYPE_PROJECT_ROOT" - roots_filename = "roots.json" - - def __init__(self, anatomy): - self.anatomy = anatomy - self.loaded_project = None - self._roots = None - - def __format__(self, *args, **kwargs): - return self.roots.__format__(*args, **kwargs) - - def __getitem__(self, key): - return self.roots[key] - - def reset(self): - """Reset current roots value.""" - self._roots = None - - def path_remapper( - self, path, dst_platform=None, src_platform=None, roots=None - ): - """Remap path for specific platform. - - Args: - path (str): Source path which need to be remapped. - dst_platform (str, optional): Specify destination platform - for which remapping should happen. - src_platform (str, optional): Specify source platform. This is - recommended to not use and keep unset until you really want - to use specific platform. - roots (dict/RootItem/None, optional): It is possible to remap - path with different roots then instance where method was - called has. - - Returns: - str/None: When path does not contain known root then - None is returned else returns remapped path with "{root}" - or "{root[]}". - """ - if roots is None: - roots = self.roots - - if roots is None: - raise ValueError("Roots are not set. Can't find path.") - - if "{root" in path: - path = path.format(**{"root": roots}) - # If `dst_platform` is not specified then return else continue. - if not dst_platform: - return path - - if isinstance(roots, RootItem): - return roots.path_remapper(path, dst_platform, src_platform) - - for _root in roots.values(): - result = self.path_remapper( - path, dst_platform, src_platform, _root - ) - if result is not None: - return result - - def find_root_template_from_path(self, path, roots=None): - """Find root value in entered path and replace it with formatting key. - - Args: - path (str): Source path where root will be searched. - roots (Roots/dict, optional): It is possible to use different - roots than instance where method was triggered has. - - Returns: - tuple: Output contains tuple with bool representing success as - first value and path with or without replaced root with - formatting key as second value. - - Raises: - ValueError: When roots are not entered and can't be loaded. - """ - if roots is None: - log.debug( - "Looking for matching root in path \"{}\".".format(path) - ) - roots = self.roots - - if roots is None: - raise ValueError("Roots are not set. Can't find path.") - - if isinstance(roots, RootItem): - return roots.find_root_template_from_path(path) - - for root_name, _root in roots.items(): - success, result = self.find_root_template_from_path(path, _root) - if success: - log.info("Found match in root \"{}\".".format(root_name)) - return success, result - - log.warning("No matching root was found in current setting.") - return (False, path) - - def set_root_environments(self): - """Set root environments for current project.""" - for key, value in self.root_environments().items(): - os.environ[key] = value - - def root_environments(self): - """Use root keys to create unique keys for environment variables. - - Concatenates prefix "OPENPYPE_ROOT" with root keys to create unique - keys. - - Returns: - dict: Result is `{(str): (str)}` dicitonary where key represents - unique key concatenated by keys and value is root value of - current platform root. - - Example: - With raw root values:: - "work": { - "windows": "P:/projects/work", - "linux": "/mnt/share/projects/work", - "darwin": "/darwin/path/work" - }, - "publish": { - "windows": "P:/projects/publish", - "linux": "/mnt/share/projects/publish", - "darwin": "/darwin/path/publish" - } - - Result on windows platform:: - { - "OPENPYPE_ROOT_WORK": "P:/projects/work", - "OPENPYPE_ROOT_PUBLISH": "P:/projects/publish" - } - - Short example when multiroot is not used:: - { - "OPENPYPE_ROOT": "P:/projects" - } - """ - return self._root_environments() - - def all_root_paths(self, roots=None): - """Return all paths for all roots of all platforms.""" - if roots is None: - roots = self.roots - - output = [] - if isinstance(roots, RootItem): - for value in roots.raw_data.values(): - output.append(value) - return output - - for _roots in roots.values(): - output.extend(self.all_root_paths(_roots)) - return output - - def _root_environments(self, keys=None, roots=None): - if not keys: - keys = [] - if roots is None: - roots = self.roots - - if isinstance(roots, RootItem): - key_items = [self.env_prefix] - for _key in keys: - key_items.append(_key.upper()) - - key = "_".join(key_items) - # Make sure key and value does not contain unicode - # - can happen in Python 2 hosts - return {str(key): str(roots.value)} - - output = {} - for _key, _value in roots.items(): - _keys = list(keys) - _keys.append(_key) - output.update(self._root_environments(_keys, _value)) - return output - - def root_environmets_fill_data(self, template=None): - """Environment variable values in dictionary for rootless path. - - Args: - template (str): Template for environment variable key fill. - By default is set to `"${}"`. - """ - if template is None: - template = "${}" - return self._root_environmets_fill_data(template) - - def _root_environmets_fill_data(self, template, keys=None, roots=None): - if keys is None and roots is None: - return { - "root": self._root_environmets_fill_data( - template, [], self.roots - ) - } - - if isinstance(roots, RootItem): - key_items = [Roots.env_prefix] - for _key in keys: - key_items.append(_key.upper()) - key = "_".join(key_items) - return template.format(key) - - output = {} - for key, value in roots.items(): - _keys = list(keys) - _keys.append(key) - output[key] = self._root_environmets_fill_data( - template, _keys, value - ) - return output - - @property - def project_name(self): - """Return project name which will be used for loading root values.""" - return self.anatomy.project_name - - @property - def roots(self): - """Property for filling "root" key in templates. - - This property returns roots for current project or default root values. - Warning: - Default roots value may cause issues when project use different - roots settings. That may happen when project use multiroot - templates but default roots miss their keys. - """ - if self.project_name != self.loaded_project: - self._roots = None - - if self._roots is None: - self._roots = self._discover() - self.loaded_project = self.project_name - return self._roots - - def _discover(self): - """ Loads current project's roots or default. - - Default roots are loaded if project override's does not contain roots. - - Returns: - `RootItem` or `dict` with multiple `RootItem`s when multiroot - setting is used. - """ - - return self._parse_dict(self.anatomy["roots"], parent=self) - - @staticmethod - def _parse_dict(data, key=None, parent_keys=None, parent=None): - """Parse roots raw data into RootItem or dictionary with RootItems. - - Converting raw roots data to `RootItem` helps to handle platform keys. - This method is recursive to be able handle multiroot setup and - is static to be able to load default roots without creating new object. - - Args: - data (dict): Should contain raw roots data to be parsed. - key (str, optional): Current root key. Set by recursion. - parent_keys (list): Parent dictionary keys. Set by recursion. - parent (Roots, optional): Parent object set in `RootItem` - helps to keep RootItem instance updated with `Roots` object. - - Returns: - `RootItem` or `dict` with multiple `RootItem`s when multiroot - setting is used. - """ - if not parent_keys: - parent_keys = [] - is_last = False - for value in data.values(): - if isinstance(value, StringType): - is_last = True - break - - if is_last: - return RootItem(data, key, parent_keys, parent=parent) - - output = {} - for _key, value in data.items(): - _parent_keys = list(parent_keys) - _parent_keys.append(_key) - output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent) - return output +def Anatomy(*args, **kwargs): + from openpype.pipeline import Anatomy + return Anatomy(*args, **kwargs) diff --git a/openpype/pipeline/anatomy.py b/openpype/pipeline/anatomy.py new file mode 100644 index 0000000000..33dfe64cb0 --- /dev/null +++ b/openpype/pipeline/anatomy.py @@ -0,0 +1,1260 @@ +import os +import re +import copy +import platform +import collections +import numbers + +from openpype.settings.lib import ( + get_default_anatomy_settings, + get_anatomy_settings +) +from openpype.lib.path_templates import ( + TemplateUnsolved, + TemplateResult, + TemplatesDict, + FormatObject, +) +from openpype.lib.log import PypeLogger + +log = PypeLogger.get_logger(__name__) + +try: + StringType = basestring +except NameError: + StringType = str + + +class ProjectNotSet(Exception): + """Exception raised when is created Anatomy without project name.""" + + +class RootCombinationError(Exception): + """This exception is raised when templates has combined root types.""" + + def __init__(self, roots): + joined_roots = ", ".join( + ["\"{}\"".format(_root) for _root in roots] + ) + # TODO better error message + msg = ( + "Combination of root with and" + " without root name in AnatomyTemplates. {}" + ).format(joined_roots) + + super(RootCombinationError, self).__init__(msg) + + +class Anatomy: + """Anatomy module helps to keep project settings. + + Wraps key project specifications, AnatomyTemplates and Roots. + + Args: + project_name (str): Project name to look on overrides. + """ + + root_key_regex = re.compile(r"{(root?[^}]+)}") + root_name_regex = re.compile(r"root\[([^]]+)\]") + + def __init__(self, project_name=None, site_name=None): + if not project_name: + project_name = os.environ.get("AVALON_PROJECT") + + if not project_name: + raise ProjectNotSet(( + "Implementation bug: Project name is not set. Anatomy requires" + " to load data for specific project." + )) + + self.project_name = project_name + + self._data = self._prepare_anatomy_data( + get_anatomy_settings(project_name, site_name) + ) + self._site_name = site_name + self._templates_obj = AnatomyTemplates(self) + self._roots_obj = Roots(self) + + # Anatomy used as dictionary + # - implemented only getters returning copy + def __getitem__(self, key): + return copy.deepcopy(self._data[key]) + + def get(self, key, default=None): + return copy.deepcopy(self._data).get(key, default) + + def keys(self): + return copy.deepcopy(self._data).keys() + + def values(self): + return copy.deepcopy(self._data).values() + + def items(self): + return copy.deepcopy(self._data).items() + + @staticmethod + def default_data(): + """Default project anatomy data. + + Always return fresh loaded data. May be used as data for new project. + + Not used inside Anatomy itself. + """ + return get_default_anatomy_settings(clear_metadata=False) + + @staticmethod + def _prepare_anatomy_data(anatomy_data): + """Prepare anatomy data for further processing. + + Method added to replace `{task}` with `{task[name]}` in templates. + """ + templates_data = anatomy_data.get("templates") + if templates_data: + # Replace `{task}` with `{task[name]}` in templates + value_queue = collections.deque() + value_queue.append(templates_data) + while value_queue: + item = value_queue.popleft() + if not isinstance(item, dict): + continue + + for key in tuple(item.keys()): + value = item[key] + if isinstance(value, dict): + value_queue.append(value) + + elif isinstance(value, StringType): + item[key] = value.replace("{task}", "{task[name]}") + return anatomy_data + + def reset(self): + """Reset values of cached data in templates and roots objects.""" + self._data = self._prepare_anatomy_data( + get_anatomy_settings(self.project_name, self._site_name) + ) + self.templates_obj.reset() + self.roots_obj.reset() + + @property + def templates(self): + """Wrap property `templates` of Anatomy's AnatomyTemplates instance.""" + return self._templates_obj.templates + + @property + def templates_obj(self): + """Return `AnatomyTemplates` object of current Anatomy instance.""" + return self._templates_obj + + def format(self, *args, **kwargs): + """Wrap `format` method of Anatomy's `templates_obj`.""" + return self._templates_obj.format(*args, **kwargs) + + def format_all(self, *args, **kwargs): + """Wrap `format_all` method of Anatomy's `templates_obj`.""" + return self._templates_obj.format_all(*args, **kwargs) + + @property + def roots(self): + """Wrap `roots` property of Anatomy's `roots_obj`.""" + return self._roots_obj.roots + + @property + def roots_obj(self): + """Return `Roots` object of current Anatomy instance.""" + return self._roots_obj + + def root_environments(self): + """Return OPENPYPE_ROOT_* environments for current project in dict.""" + return self._roots_obj.root_environments() + + def root_environmets_fill_data(self, template=None): + """Environment variable values in dictionary for rootless path. + + Args: + template (str): Template for environment variable key fill. + By default is set to `"${}"`. + """ + return self.roots_obj.root_environmets_fill_data(template) + + def find_root_template_from_path(self, *args, **kwargs): + """Wrapper for Roots `find_root_template_from_path`.""" + return self.roots_obj.find_root_template_from_path(*args, **kwargs) + + def path_remapper(self, *args, **kwargs): + """Wrapper for Roots `path_remapper`.""" + return self.roots_obj.path_remapper(*args, **kwargs) + + def all_root_paths(self): + """Wrapper for Roots `all_root_paths`.""" + return self.roots_obj.all_root_paths() + + def set_root_environments(self): + """Set OPENPYPE_ROOT_* environments for current project.""" + self._roots_obj.set_root_environments() + + def root_names(self): + """Return root names for current project.""" + return self.root_names_from_templates(self.templates) + + def _root_keys_from_templates(self, data): + """Extract root key from templates in data. + + Args: + data (dict): Data that may contain templates as string. + + Return: + set: Set of all root names from templates as strings. + + Output example: `{"root[work]", "root[publish]"}` + """ + + output = set() + if isinstance(data, dict): + for value in data.values(): + for root in self._root_keys_from_templates(value): + output.add(root) + + elif isinstance(data, str): + for group in re.findall(self.root_key_regex, data): + output.add(group) + + return output + + def root_value_for_template(self, template): + """Returns value of root key from template.""" + root_templates = [] + for group in re.findall(self.root_key_regex, template): + root_templates.append("{" + group + "}") + + if not root_templates: + return None + + return root_templates[0].format(**{"root": self.roots}) + + def root_names_from_templates(self, templates): + """Extract root names form anatomy templates. + + Returns None if values in templates contain only "{root}". + Empty list is returned if there is no "root" in templates. + Else returns all root names from templates in list. + + RootCombinationError is raised when templates contain both root types, + basic "{root}" and with root name specification "{root[work]}". + + Args: + templates (dict): Anatomy templates where roots are not filled. + + Return: + list/None: List of all root names from templates as strings when + multiroot setup is used, otherwise None is returned. + """ + roots = list(self._root_keys_from_templates(templates)) + # Return empty list if no roots found in templates + if not roots: + return roots + + # Raise exception when root keys have roots with and without root name. + # Invalid output example: ["root", "root[project]", "root[render]"] + if len(roots) > 1 and "root" in roots: + raise RootCombinationError(roots) + + # Return None if "root" without root name in templates + if len(roots) == 1 and roots[0] == "root": + return None + + names = set() + for root in roots: + for group in re.findall(self.root_name_regex, root): + names.add(group) + return list(names) + + def fill_root(self, template_path): + """Fill template path where is only "root" key unfilled. + + Args: + template_path (str): Path with "root" key in. + Example path: "{root}/projects/MyProject/Shot01/Lighting/..." + + Return: + str: formatted path + """ + # NOTE does not care if there are different keys than "root" + return template_path.format(**{"root": self.roots}) + + @classmethod + def fill_root_with_path(cls, rootless_path, root_path): + """Fill path without filled "root" key with passed path. + + This is helper to fill root with different directory path than anatomy + has defined no matter if is single or multiroot. + + Output path is same as input path if `rootless_path` does not contain + unfilled root key. + + Args: + rootless_path (str): Path without filled "root" key. Example: + "{root[work]}/MyProject/..." + root_path (str): What should replace root key in `rootless_path`. + + Returns: + str: Path with filled root. + """ + output = str(rootless_path) + for group in re.findall(cls.root_key_regex, rootless_path): + replacement = "{" + group + "}" + output = output.replace(replacement, root_path) + + return output + + def replace_root_with_env_key(self, filepath, template=None): + """Replace root of path with environment key. + + # Example: + ## Project with roots: + ``` + { + "nas": { + "windows": P:/projects", + ... + } + ... + } + ``` + + ## Entered filepath + "P:/projects/project/asset/task/animation_v001.ma" + + ## Entered template + "<{}>" + + ## Output + "/project/asset/task/animation_v001.ma" + + Args: + filepath (str): Full file path where root should be replaced. + template (str): Optional template for environment key. Must + have one index format key. + Default value if not entered: "${}" + + Returns: + str: Path where root is replaced with environment root key. + + Raise: + ValueError: When project's roots were not found in entered path. + """ + success, rootless_path = self.find_root_template_from_path(filepath) + if not success: + raise ValueError( + "{}: Project's roots were not found in path: {}".format( + self.project_name, filepath + ) + ) + + data = self.root_environmets_fill_data(template) + return rootless_path.format(**data) + + +class AnatomyTemplateUnsolved(TemplateUnsolved): + """Exception for unsolved template when strict is set to True.""" + + msg = "Anatomy template \"{0}\" is unsolved.{1}{2}" + + +class AnatomyTemplateResult(TemplateResult): + rootless = None + + def __new__(cls, result, rootless_path): + new_obj = super(AnatomyTemplateResult, cls).__new__( + cls, + str(result), + result.template, + result.solved, + result.used_values, + result.missing_keys, + result.invalid_types + ) + new_obj.rootless = rootless_path + return new_obj + + def validate(self): + if not self.solved: + raise AnatomyTemplateUnsolved( + self.template, + self.missing_keys, + self.invalid_types + ) + + +class AnatomyTemplates(TemplatesDict): + inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") + inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}") + + def __init__(self, anatomy): + super(AnatomyTemplates, self).__init__() + self.anatomy = anatomy + self.loaded_project = None + + def __getitem__(self, key): + return self.templates[key] + + def get(self, key, default=None): + return self.templates.get(key, default) + + def reset(self): + self._raw_templates = None + self._templates = None + self._objected_templates = None + + @property + def project_name(self): + return self.anatomy.project_name + + @property + def roots(self): + return self.anatomy.roots + + @property + def templates(self): + self._validate_discovery() + return self._templates + + @property + def objected_templates(self): + self._validate_discovery() + return self._objected_templates + + def _validate_discovery(self): + if self.project_name != self.loaded_project: + self.reset() + + if self._templates is None: + self._discover() + self.loaded_project = self.project_name + + def _format_value(self, value, data): + if isinstance(value, RootItem): + return self._solve_dict(value, data) + + result = super(AnatomyTemplates, self)._format_value(value, data) + if isinstance(result, TemplateResult): + rootless_path = self._rootless_path(result, data) + result = AnatomyTemplateResult(result, rootless_path) + return result + + def set_templates(self, templates): + if not templates: + self.reset() + return + + self._raw_templates = copy.deepcopy(templates) + templates = copy.deepcopy(templates) + v_queue = collections.deque() + v_queue.append(templates) + while v_queue: + item = v_queue.popleft() + if not isinstance(item, dict): + continue + + for key in tuple(item.keys()): + value = item[key] + if isinstance(value, dict): + v_queue.append(value) + + elif ( + isinstance(value, StringType) + and "{task}" in value + ): + item[key] = value.replace("{task}", "{task[name]}") + + solved_templates = self.solve_template_inner_links(templates) + self._templates = solved_templates + self._objected_templates = self.create_ojected_templates( + solved_templates + ) + + def default_templates(self): + """Return default templates data with solved inner keys.""" + return self.solve_template_inner_links( + self.anatomy["templates"] + ) + + def _discover(self): + """ Loads anatomy templates from yaml. + Default templates are loaded if project is not set or project does + not have set it's own. + TODO: create templates if not exist. + + Returns: + TemplatesResultDict: Contain templates data for current project of + default templates. + """ + + if self.project_name is None: + # QUESTION create project specific if not found? + raise AssertionError(( + "Project \"{0}\" does not have his own templates." + " Trying to use default." + ).format(self.project_name)) + + self.set_templates(self.anatomy["templates"]) + + @classmethod + def replace_inner_keys(cls, matches, value, key_values, key): + """Replacement of inner keys in template values.""" + for match in matches: + anatomy_sub_keys = ( + cls.inner_key_name_pattern.findall(match) + ) + if key in anatomy_sub_keys: + raise ValueError(( + "Unsolvable recursion in inner keys, " + "key: \"{}\" is in his own value." + " Can't determine source, please check Anatomy templates." + ).format(key)) + + for anatomy_sub_key in anatomy_sub_keys: + replace_value = key_values.get(anatomy_sub_key) + if replace_value is None: + raise KeyError(( + "Anatomy templates can't be filled." + " Anatomy key `{0}` has" + " invalid inner key `{1}`." + ).format(key, anatomy_sub_key)) + + valid = isinstance(replace_value, (numbers.Number, StringType)) + if not valid: + raise ValueError(( + "Anatomy templates can't be filled." + " Anatomy key `{0}` has" + " invalid inner key `{1}`" + " with value `{2}`." + ).format(key, anatomy_sub_key, str(replace_value))) + + value = value.replace(match, str(replace_value)) + + return value + + @classmethod + def prepare_inner_keys(cls, key_values): + """Check values of inner keys. + + Check if inner key exist in template group and has valid value. + It is also required to avoid infinite loop with unsolvable recursion + when first inner key's value refers to second inner key's value where + first is used. + """ + keys_to_solve = set(key_values.keys()) + while True: + found = False + for key in tuple(keys_to_solve): + value = key_values[key] + + if isinstance(value, StringType): + matches = cls.inner_key_pattern.findall(value) + if not matches: + keys_to_solve.remove(key) + continue + + found = True + key_values[key] = cls.replace_inner_keys( + matches, value, key_values, key + ) + continue + + elif not isinstance(value, dict): + keys_to_solve.remove(key) + continue + + subdict_found = False + for _key, _value in tuple(value.items()): + matches = cls.inner_key_pattern.findall(_value) + if not matches: + continue + + subdict_found = True + found = True + key_values[key][_key] = cls.replace_inner_keys( + matches, _value, key_values, + "{}.{}".format(key, _key) + ) + + if not subdict_found: + keys_to_solve.remove(key) + + if not found: + break + + return key_values + + @classmethod + def solve_template_inner_links(cls, templates): + """Solve templates inner keys identified by "{@*}". + + Process is split into 2 parts. + First is collecting all global keys (keys in top hierarchy where value + is not dictionary). All global keys are set for all group keys (keys + in top hierarchy where value is dictionary). Value of a key is not + overridden in group if already contain value for the key. + + In second part all keys with "at" symbol in value are replaced with + value of the key afterward "at" symbol from the group. + + Args: + templates (dict): Raw templates data. + + Example: + templates:: + key_1: "value_1", + key_2: "{@key_1}/{filling_key}" + + group_1: + key_3: "value_3/{@key_2}" + + group_2: + key_2": "value_2" + key_4": "value_4/{@key_2}" + + output:: + key_1: "value_1" + key_2: "value_1/{filling_key}" + + group_1: { + key_1: "value_1" + key_2: "value_1/{filling_key}" + key_3: "value_3/value_1/{filling_key}" + + group_2: { + key_1: "value_1" + key_2: "value_2" + key_4: "value_3/value_2" + """ + default_key_values = templates.pop("defaults", {}) + for key, value in tuple(templates.items()): + if isinstance(value, dict): + continue + default_key_values[key] = templates.pop(key) + + # Pop "others" key before before expected keys are processed + other_templates = templates.pop("others") or {} + + keys_by_subkey = {} + for sub_key, sub_value in templates.items(): + key_values = {} + key_values.update(default_key_values) + key_values.update(sub_value) + keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) + + for sub_key, sub_value in other_templates.items(): + if sub_key in keys_by_subkey: + log.warning(( + "Key \"{}\" is duplicated in others. Skipping." + ).format(sub_key)) + continue + + key_values = {} + key_values.update(default_key_values) + key_values.update(sub_value) + keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) + + default_keys_by_subkeys = cls.prepare_inner_keys(default_key_values) + + for key, value in default_keys_by_subkeys.items(): + keys_by_subkey[key] = value + + return keys_by_subkey + + def _dict_to_subkeys_list(self, subdict, pre_keys=None): + if pre_keys is None: + pre_keys = [] + output = [] + for key in subdict: + value = subdict[key] + result = list(pre_keys) + result.append(key) + if isinstance(value, dict): + for item in self._dict_to_subkeys_list(value, result): + output.append(item) + else: + output.append(result) + return output + + def _keys_to_dicts(self, key_list, value): + if not key_list: + return None + if len(key_list) == 1: + return {key_list[0]: value} + return {key_list[0]: self._keys_to_dicts(key_list[1:], value)} + + def _rootless_path(self, result, final_data): + used_values = result.used_values + missing_keys = result.missing_keys + template = result.template + invalid_types = result.invalid_types + if ( + "root" not in used_values + or "root" in missing_keys + or "{root" not in template + ): + return + + for invalid_type in invalid_types: + if "root" in invalid_type: + return + + root_keys = self._dict_to_subkeys_list({"root": used_values["root"]}) + if not root_keys: + return + + output = str(result) + for used_root_keys in root_keys: + if not used_root_keys: + continue + + used_value = used_values + root_key = None + for key in used_root_keys: + used_value = used_value[key] + if root_key is None: + root_key = key + else: + root_key += "[{}]".format(key) + + root_key = "{" + root_key + "}" + output = output.replace(str(used_value), root_key) + + return output + + def format(self, data, strict=True): + copy_data = copy.deepcopy(data) + roots = self.roots + if roots: + copy_data["root"] = roots + result = super(AnatomyTemplates, self).format(copy_data) + result.strict = strict + return result + + def format_all(self, in_data, only_keys=True): + """ Solves templates based on entered data. + + Args: + data (dict): Containing keys to be filled into template. + + Returns: + TemplatesResultDict: Output `TemplateResult` have `strict` + attribute set to False so accessing unfilled keys in templates + won't raise any exceptions. + """ + return self.format(in_data, strict=False) + + +class RootItem(FormatObject): + """Represents one item or roots. + + Holds raw data of root item specification. Raw data contain value + for each platform, but current platform value is used when object + is used for formatting of template. + + Args: + root_raw_data (dict): Dictionary containing root values by platform + names. ["windows", "linux" and "darwin"] + name (str, optional): Root name which is representing. Used with + multi root setup otherwise None value is expected. + parent_keys (list, optional): All dictionary parent keys. Values of + `parent_keys` are used for get full key which RootItem is + representing. Used for replacing root value in path with + formattable key. e.g. parent_keys == ["work"] -> {root[work]} + parent (object, optional): It is expected to be `Roots` object. + Value of `parent` won't affect code logic much. + """ + + def __init__( + self, root_raw_data, name=None, parent_keys=None, parent=None + ): + lowered_platform_keys = {} + for key, value in root_raw_data.items(): + lowered_platform_keys[key.lower()] = value + self.raw_data = lowered_platform_keys + self.cleaned_data = self._clean_roots(lowered_platform_keys) + self.name = name + self.parent_keys = parent_keys or [] + self.parent = parent + + self.available_platforms = list(lowered_platform_keys.keys()) + self.value = lowered_platform_keys.get(platform.system().lower()) + self.clean_value = self.clean_root(self.value) + + def __format__(self, *args, **kwargs): + return self.value.__format__(*args, **kwargs) + + def __str__(self): + return str(self.value) + + def __repr__(self): + return self.__str__() + + def __getitem__(self, key): + if isinstance(key, numbers.Number): + return self.value[key] + + additional_info = "" + if self.parent and self.parent.project_name: + additional_info += " for project \"{}\"".format( + self.parent.project_name + ) + + raise AssertionError( + "Root key \"{}\" is missing{}.".format( + key, additional_info + ) + ) + + def full_key(self): + """Full key value for dictionary formatting in template. + + Returns: + str: Return full replacement key for formatting. This helps when + multiple roots are set. In that case e.g. `"root[work]"` is + returned. + """ + if not self.name: + return "root" + + joined_parent_keys = "".join( + ["[{}]".format(key) for key in self.parent_keys] + ) + return "root{}".format(joined_parent_keys) + + def clean_path(self, path): + """Just replace backslashes with forward slashes.""" + return str(path).replace("\\", "/") + + def clean_root(self, root): + """Makes sure root value does not end with slash.""" + if root: + root = self.clean_path(root) + while root.endswith("/"): + root = root[:-1] + return root + + def _clean_roots(self, raw_data): + """Clean all values of raw root item values.""" + cleaned = {} + for key, value in raw_data.items(): + cleaned[key] = self.clean_root(value) + return cleaned + + def path_remapper(self, path, dst_platform=None, src_platform=None): + """Remap path for specific platform. + + Args: + path (str): Source path which need to be remapped. + dst_platform (str, optional): Specify destination platform + for which remapping should happen. + src_platform (str, optional): Specify source platform. This is + recommended to not use and keep unset until you really want + to use specific platform. + roots (dict/RootItem/None, optional): It is possible to remap + path with different roots then instance where method was + called has. + + Returns: + str/None: When path does not contain known root then + None is returned else returns remapped path with "{root}" + or "{root[]}". + """ + cleaned_path = self.clean_path(path) + if dst_platform: + dst_root_clean = self.cleaned_data.get(dst_platform) + if not dst_root_clean: + key_part = "" + full_key = self.full_key() + if full_key != "root": + key_part += "\"{}\" ".format(full_key) + + log.warning( + "Root {}miss platform \"{}\" definition.".format( + key_part, dst_platform + ) + ) + return None + + if cleaned_path.startswith(dst_root_clean): + return cleaned_path + + if src_platform: + src_root_clean = self.cleaned_data.get(src_platform) + if src_root_clean is None: + log.warning( + "Root \"{}\" miss platform \"{}\" definition.".format( + self.full_key(), src_platform + ) + ) + return None + + if not cleaned_path.startswith(src_root_clean): + return None + + subpath = cleaned_path[len(src_root_clean):] + if dst_platform: + # `dst_root_clean` is used from upper condition + return dst_root_clean + subpath + return self.clean_value + subpath + + result, template = self.find_root_template_from_path(path) + if not result: + return None + + def parent_dict(keys, value): + if not keys: + return value + + key = keys.pop(0) + return {key: parent_dict(keys, value)} + + if dst_platform: + format_value = parent_dict(list(self.parent_keys), dst_root_clean) + else: + format_value = parent_dict(list(self.parent_keys), self.value) + + return template.format(**{"root": format_value}) + + def find_root_template_from_path(self, path): + """Replaces known root value with formattable key in path. + + All platform values are checked for this replacement. + + Args: + path (str): Path where root value should be found. + + Returns: + tuple: Tuple contain 2 values: `success` (bool) and `path` (str). + When success it True then path should contain replaced root + value with formattable key. + + Example: + When input path is:: + "C:/windows/path/root/projects/my_project/file.ext" + + And raw data of item looks like:: + { + "windows": "C:/windows/path/root", + "linux": "/mount/root" + } + + Output will be:: + (True, "{root}/projects/my_project/file.ext") + + If any of raw data value wouldn't match path's root output is:: + (False, "C:/windows/path/root/projects/my_project/file.ext") + """ + result = False + output = str(path) + + root_paths = list(self.cleaned_data.values()) + mod_path = self.clean_path(path) + for root_path in root_paths: + # Skip empty paths + if not root_path: + continue + + if mod_path.startswith(root_path): + result = True + replacement = "{" + self.full_key() + "}" + output = replacement + mod_path[len(root_path):] + break + + return (result, output) + + +class Roots: + """Object which should be used for formatting "root" key in templates. + + Args: + anatomy Anatomy: Anatomy object created for a specific project. + """ + + env_prefix = "OPENPYPE_PROJECT_ROOT" + roots_filename = "roots.json" + + def __init__(self, anatomy): + self.anatomy = anatomy + self.loaded_project = None + self._roots = None + + def __format__(self, *args, **kwargs): + return self.roots.__format__(*args, **kwargs) + + def __getitem__(self, key): + return self.roots[key] + + def reset(self): + """Reset current roots value.""" + self._roots = None + + def path_remapper( + self, path, dst_platform=None, src_platform=None, roots=None + ): + """Remap path for specific platform. + + Args: + path (str): Source path which need to be remapped. + dst_platform (str, optional): Specify destination platform + for which remapping should happen. + src_platform (str, optional): Specify source platform. This is + recommended to not use and keep unset until you really want + to use specific platform. + roots (dict/RootItem/None, optional): It is possible to remap + path with different roots then instance where method was + called has. + + Returns: + str/None: When path does not contain known root then + None is returned else returns remapped path with "{root}" + or "{root[]}". + """ + if roots is None: + roots = self.roots + + if roots is None: + raise ValueError("Roots are not set. Can't find path.") + + if "{root" in path: + path = path.format(**{"root": roots}) + # If `dst_platform` is not specified then return else continue. + if not dst_platform: + return path + + if isinstance(roots, RootItem): + return roots.path_remapper(path, dst_platform, src_platform) + + for _root in roots.values(): + result = self.path_remapper( + path, dst_platform, src_platform, _root + ) + if result is not None: + return result + + def find_root_template_from_path(self, path, roots=None): + """Find root value in entered path and replace it with formatting key. + + Args: + path (str): Source path where root will be searched. + roots (Roots/dict, optional): It is possible to use different + roots than instance where method was triggered has. + + Returns: + tuple: Output contains tuple with bool representing success as + first value and path with or without replaced root with + formatting key as second value. + + Raises: + ValueError: When roots are not entered and can't be loaded. + """ + if roots is None: + log.debug( + "Looking for matching root in path \"{}\".".format(path) + ) + roots = self.roots + + if roots is None: + raise ValueError("Roots are not set. Can't find path.") + + if isinstance(roots, RootItem): + return roots.find_root_template_from_path(path) + + for root_name, _root in roots.items(): + success, result = self.find_root_template_from_path(path, _root) + if success: + log.info("Found match in root \"{}\".".format(root_name)) + return success, result + + log.warning("No matching root was found in current setting.") + return (False, path) + + def set_root_environments(self): + """Set root environments for current project.""" + for key, value in self.root_environments().items(): + os.environ[key] = value + + def root_environments(self): + """Use root keys to create unique keys for environment variables. + + Concatenates prefix "OPENPYPE_ROOT" with root keys to create unique + keys. + + Returns: + dict: Result is `{(str): (str)}` dicitonary where key represents + unique key concatenated by keys and value is root value of + current platform root. + + Example: + With raw root values:: + "work": { + "windows": "P:/projects/work", + "linux": "/mnt/share/projects/work", + "darwin": "/darwin/path/work" + }, + "publish": { + "windows": "P:/projects/publish", + "linux": "/mnt/share/projects/publish", + "darwin": "/darwin/path/publish" + } + + Result on windows platform:: + { + "OPENPYPE_ROOT_WORK": "P:/projects/work", + "OPENPYPE_ROOT_PUBLISH": "P:/projects/publish" + } + + Short example when multiroot is not used:: + { + "OPENPYPE_ROOT": "P:/projects" + } + """ + return self._root_environments() + + def all_root_paths(self, roots=None): + """Return all paths for all roots of all platforms.""" + if roots is None: + roots = self.roots + + output = [] + if isinstance(roots, RootItem): + for value in roots.raw_data.values(): + output.append(value) + return output + + for _roots in roots.values(): + output.extend(self.all_root_paths(_roots)) + return output + + def _root_environments(self, keys=None, roots=None): + if not keys: + keys = [] + if roots is None: + roots = self.roots + + if isinstance(roots, RootItem): + key_items = [self.env_prefix] + for _key in keys: + key_items.append(_key.upper()) + + key = "_".join(key_items) + # Make sure key and value does not contain unicode + # - can happen in Python 2 hosts + return {str(key): str(roots.value)} + + output = {} + for _key, _value in roots.items(): + _keys = list(keys) + _keys.append(_key) + output.update(self._root_environments(_keys, _value)) + return output + + def root_environmets_fill_data(self, template=None): + """Environment variable values in dictionary for rootless path. + + Args: + template (str): Template for environment variable key fill. + By default is set to `"${}"`. + """ + if template is None: + template = "${}" + return self._root_environmets_fill_data(template) + + def _root_environmets_fill_data(self, template, keys=None, roots=None): + if keys is None and roots is None: + return { + "root": self._root_environmets_fill_data( + template, [], self.roots + ) + } + + if isinstance(roots, RootItem): + key_items = [Roots.env_prefix] + for _key in keys: + key_items.append(_key.upper()) + key = "_".join(key_items) + return template.format(key) + + output = {} + for key, value in roots.items(): + _keys = list(keys) + _keys.append(key) + output[key] = self._root_environmets_fill_data( + template, _keys, value + ) + return output + + @property + def project_name(self): + """Return project name which will be used for loading root values.""" + return self.anatomy.project_name + + @property + def roots(self): + """Property for filling "root" key in templates. + + This property returns roots for current project or default root values. + Warning: + Default roots value may cause issues when project use different + roots settings. That may happen when project use multiroot + templates but default roots miss their keys. + """ + if self.project_name != self.loaded_project: + self._roots = None + + if self._roots is None: + self._roots = self._discover() + self.loaded_project = self.project_name + return self._roots + + def _discover(self): + """ Loads current project's roots or default. + + Default roots are loaded if project override's does not contain roots. + + Returns: + `RootItem` or `dict` with multiple `RootItem`s when multiroot + setting is used. + """ + + return self._parse_dict(self.anatomy["roots"], parent=self) + + @staticmethod + def _parse_dict(data, key=None, parent_keys=None, parent=None): + """Parse roots raw data into RootItem or dictionary with RootItems. + + Converting raw roots data to `RootItem` helps to handle platform keys. + This method is recursive to be able handle multiroot setup and + is static to be able to load default roots without creating new object. + + Args: + data (dict): Should contain raw roots data to be parsed. + key (str, optional): Current root key. Set by recursion. + parent_keys (list): Parent dictionary keys. Set by recursion. + parent (Roots, optional): Parent object set in `RootItem` + helps to keep RootItem instance updated with `Roots` object. + + Returns: + `RootItem` or `dict` with multiple `RootItem`s when multiroot + setting is used. + """ + if not parent_keys: + parent_keys = [] + is_last = False + for value in data.values(): + if isinstance(value, StringType): + is_last = True + break + + if is_last: + return RootItem(data, key, parent_keys, parent=parent) + + output = {} + for _key, value in data.items(): + _parent_keys = list(parent_keys) + _parent_keys.append(_key) + output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent) + return output From f0a5daa6434086453b8b3ff1e5ccd57bf055228b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 14:05:45 +0200 Subject: [PATCH 097/129] use six.string_types to determine string type --- openpype/pipeline/anatomy.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/pipeline/anatomy.py b/openpype/pipeline/anatomy.py index 33dfe64cb0..b5b87b6432 100644 --- a/openpype/pipeline/anatomy.py +++ b/openpype/pipeline/anatomy.py @@ -5,6 +5,8 @@ import platform import collections import numbers +import six + from openpype.settings.lib import ( get_default_anatomy_settings, get_anatomy_settings @@ -19,11 +21,6 @@ from openpype.lib.log import PypeLogger log = PypeLogger.get_logger(__name__) -try: - StringType = basestring -except NameError: - StringType = str - class ProjectNotSet(Exception): """Exception raised when is created Anatomy without project name.""" @@ -124,7 +121,7 @@ class Anatomy: if isinstance(value, dict): value_queue.append(value) - elif isinstance(value, StringType): + elif isinstance(value, six.string_types): item[key] = value.replace("{task}", "{task[name]}") return anatomy_data @@ -462,7 +459,7 @@ class AnatomyTemplates(TemplatesDict): v_queue.append(value) elif ( - isinstance(value, StringType) + isinstance(value, six.string_types) and "{task}" in value ): item[key] = value.replace("{task}", "{task[name]}") @@ -522,8 +519,10 @@ class AnatomyTemplates(TemplatesDict): " invalid inner key `{1}`." ).format(key, anatomy_sub_key)) - valid = isinstance(replace_value, (numbers.Number, StringType)) - if not valid: + if not ( + isinstance(replace_value, numbers.Number) + or isinstance(replace_value, six.string_types) + ): raise ValueError(( "Anatomy templates can't be filled." " Anatomy key `{0}` has" @@ -550,7 +549,7 @@ class AnatomyTemplates(TemplatesDict): for key in tuple(keys_to_solve): value = key_values[key] - if isinstance(value, StringType): + if isinstance(value, six.string_types): matches = cls.inner_key_pattern.findall(value) if not matches: keys_to_solve.remove(key) @@ -1245,7 +1244,7 @@ class Roots: parent_keys = [] is_last = False for value in data.values(): - if isinstance(value, StringType): + if isinstance(value, six.string_types): is_last = True break From 929fe06127e6960b17327bdffbfaf061e767b93f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 14:13:25 +0200 Subject: [PATCH 098/129] added deprecation warning to anatomy lib --- openpype/lib/anatomy.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py index b62b207ade..6d339f058f 100644 --- a/openpype/lib/anatomy.py +++ b/openpype/lib/anatomy.py @@ -1,3 +1,38 @@ +"""Code related to project Anatomy was moved +to 'openpype.pipeline.anatomy' please change your imports as soon as +possible. File will be probably removed in OpenPype 3.14.* +""" + +import warnings +import functools + + +class AnatomyDeprecatedWarning(DeprecationWarning): + pass + + +def anatomy_deprecated(func): + """Mark functions as deprecated. + + It will result in a warning being emitted when the function is used. + """ + + @functools.wraps(func) + def new_func(*args, **kwargs): + warnings.simplefilter("always", AnatomyDeprecatedWarning) + warnings.warn( + ( + "Deprecated import of 'Anatomy'." + " Class was moved to 'openpype.pipeline.anatomy'." + " Please change your imports of Anatomy in codebase." + ), + category=AnatomyDeprecatedWarning + ) + return func(*args, **kwargs) + return new_func + + +@anatomy_deprecated def Anatomy(*args, **kwargs): - from openpype.pipeline import Anatomy + from openpype.pipeline.anatomy import Anatomy return Anatomy(*args, **kwargs) From ffb2a8c33aed22539675462ad0f040f717dca29c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 14:14:21 +0200 Subject: [PATCH 099/129] removed unused method --- openpype/pipeline/anatomy.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/openpype/pipeline/anatomy.py b/openpype/pipeline/anatomy.py index b5b87b6432..8a05a3794d 100644 --- a/openpype/pipeline/anatomy.py +++ b/openpype/pipeline/anatomy.py @@ -7,10 +7,7 @@ import numbers import six -from openpype.settings.lib import ( - get_default_anatomy_settings, - get_anatomy_settings -) +from openpype.settings.lib import get_anatomy_settings from openpype.lib.path_templates import ( TemplateUnsolved, TemplateResult, @@ -90,16 +87,6 @@ class Anatomy: def items(self): return copy.deepcopy(self._data).items() - @staticmethod - def default_data(): - """Default project anatomy data. - - Always return fresh loaded data. May be used as data for new project. - - Not used inside Anatomy itself. - """ - return get_default_anatomy_settings(clear_metadata=False) - @staticmethod def _prepare_anatomy_data(anatomy_data): """Prepare anatomy data for further processing. From bcaafb479d42d099f22e4f96a9f9fe196b32390c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 14:40:40 +0200 Subject: [PATCH 100/129] added Anatomy to openpype.pipeline init file --- openpype/pipeline/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 2e441fbf27..2cf785d981 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -6,6 +6,7 @@ from .constants import ( from .mongodb import ( AvalonMongoDB, ) +from .anatomy import Anatomy from .create import ( BaseCreator, @@ -96,6 +97,9 @@ __all__ = ( # --- MongoDB --- "AvalonMongoDB", + # --- Anatomy --- + "Anatomy", + # --- Create --- "BaseCreator", "Creator", From a4e371889a61d3f7a36874e00c92ec99db196258 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 14:44:12 +0200 Subject: [PATCH 101/129] fix anatomy result copy --- openpype/pipeline/anatomy.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/pipeline/anatomy.py b/openpype/pipeline/anatomy.py index 8a05a3794d..73081f18fb 100644 --- a/openpype/pipeline/anatomy.py +++ b/openpype/pipeline/anatomy.py @@ -369,6 +369,17 @@ class AnatomyTemplateResult(TemplateResult): self.invalid_types ) + def copy(self): + tmp = TemplateResult( + str(self), + self.template, + self.solved, + self.used_values, + self.missing_keys, + self.invalid_types + ) + return self.__class__(tmp, self.rootless) + class AnatomyTemplates(TemplatesDict): inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})") From af2c57674eb9263587ec082db70e12f69ccf2555 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 15:21:21 +0200 Subject: [PATCH 102/129] use new Anatomy source in tools --- openpype/tools/loader/widgets.py | 3 +-- openpype/tools/texture_copy/app.py | 3 +-- openpype/tools/workfiles/files_widget.py | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 1f6d8b9fa2..13e18b3757 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -17,8 +17,7 @@ from openpype.client import ( get_thumbnail_id_from_source, get_thumbnail, ) -from openpype.api import Anatomy -from openpype.pipeline import HeroVersionType +from openpype.pipeline import HeroVersionType, Anatomy from openpype.pipeline.thumbnail import get_thumbnail_binary from openpype.pipeline.load import ( discover_loader_plugins, diff --git a/openpype/tools/texture_copy/app.py b/openpype/tools/texture_copy/app.py index 746a72b3ec..a695bb8c4d 100644 --- a/openpype/tools/texture_copy/app.py +++ b/openpype/tools/texture_copy/app.py @@ -6,8 +6,7 @@ import speedcopy from openpype.client import get_project, get_asset_by_name from openpype.lib import Terminal -from openpype.api import Anatomy -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, Anatomy t = Terminal() diff --git a/openpype/tools/workfiles/files_widget.py b/openpype/tools/workfiles/files_widget.py index a7e54471dc..c92e4fe904 100644 --- a/openpype/tools/workfiles/files_widget.py +++ b/openpype/tools/workfiles/files_widget.py @@ -11,7 +11,6 @@ from openpype.tools.utils import PlaceholderLineEdit from openpype.tools.utils.delegates import PrettyTimeDelegate from openpype.lib import ( emit_event, - Anatomy, get_workfile_template_key, create_workdir_extra_folders, ) @@ -22,6 +21,7 @@ from openpype.lib.avalon_context import ( from openpype.pipeline import ( registered_host, legacy_io, + Anatomy, ) from .model import ( WorkAreaFilesModel, From b6ea950b3a091053b034d90ce82f421e9cb25be9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 15:22:49 +0200 Subject: [PATCH 103/129] use new Anatomy import in rest of pipeline --- openpype/pipeline/context_tools.py | 7 ++----- openpype/pipeline/load/utils.py | 2 +- openpype/plugins/load/delete_old_versions.py | 3 +-- openpype/plugins/load/delivery.py | 4 ++-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 4a147c230b..047482f6ff 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -14,11 +14,8 @@ from pyblish.lib import MessageHandler import openpype from openpype.modules import load_modules, ModulesManager from openpype.settings import get_project_settings -from openpype.lib import ( - Anatomy, - filter_pyblish_plugins, -) - +from openpype.lib import filter_pyblish_plugins +from .anatomy import Anatomy from . import ( legacy_io, register_loader_plugin_path, diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 99e5d11f82..e813b7934f 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -9,10 +9,10 @@ import numbers import six from bson.objectid import ObjectId -from openpype.lib import Anatomy from openpype.pipeline import ( schema, legacy_io, + Anatomy, ) log = logging.getLogger(__name__) diff --git a/openpype/plugins/load/delete_old_versions.py b/openpype/plugins/load/delete_old_versions.py index c3e9e9fa0a..7465f53855 100644 --- a/openpype/plugins/load/delete_old_versions.py +++ b/openpype/plugins/load/delete_old_versions.py @@ -9,9 +9,8 @@ import qargparse from Qt import QtWidgets, QtCore from openpype import style -from openpype.pipeline import load, AvalonMongoDB +from openpype.pipeline import load, AvalonMongoDB, Anatomy from openpype.lib import StringTemplate -from openpype.api import Anatomy class DeleteOldVersions(load.SubsetLoaderPlugin): diff --git a/openpype/plugins/load/delivery.py b/openpype/plugins/load/delivery.py index 7df07e3f64..0361ab2be5 100644 --- a/openpype/plugins/load/delivery.py +++ b/openpype/plugins/load/delivery.py @@ -3,8 +3,8 @@ from collections import defaultdict from Qt import QtWidgets, QtCore, QtGui -from openpype.pipeline import load, AvalonMongoDB -from openpype.api import Anatomy, config +from openpype.lib import config +from openpype.pipeline import load, AvalonMongoDB, Anatomy from openpype import resources, style from openpype.lib.delivery import ( From 168c3b38a46d656f31c14b940fbd6b533a926766 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 15:28:59 +0200 Subject: [PATCH 104/129] modified imports in sync server --- openpype/modules/sync_server/providers/local_drive.py | 5 +++-- openpype/modules/sync_server/sync_server_module.py | 10 ++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/modules/sync_server/providers/local_drive.py b/openpype/modules/sync_server/providers/local_drive.py index 68f604b39c..172cb338cf 100644 --- a/openpype/modules/sync_server/providers/local_drive.py +++ b/openpype/modules/sync_server/providers/local_drive.py @@ -4,10 +4,11 @@ import shutil import threading import time -from openpype.api import Logger, Anatomy +from openpype.api import Logger +from openpype.pipeline import Anatomy from .abstract_provider import AbstractProvider -log = Logger().get_logger("SyncServer") +log = Logger.get_logger("SyncServer") class LocalDriveHandler(AbstractProvider): diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 698b296a52..4027561d22 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -9,14 +9,12 @@ from collections import deque, defaultdict from openpype.modules import OpenPypeModule from openpype_interfaces import ITrayModule -from openpype.api import ( - Anatomy, +from openpype.settings import ( get_project_settings, get_system_settings, - get_local_site_id ) -from openpype.lib import PypeLogger -from openpype.pipeline import AvalonMongoDB +from openpype.lib import PypeLogger, get_local_site_id +from openpype.pipeline import AvalonMongoDB, Anatomy from openpype.settings.lib import ( get_default_anatomy_settings, get_anatomy_settings @@ -28,7 +26,7 @@ from .providers import lib from .utils import time_function, SyncStatus, SiteAlreadyPresentError -log = PypeLogger().get_logger("SyncServer") +log = PypeLogger.get_logger("SyncServer") class SyncServerModule(OpenPypeModule, ITrayModule): From 05fe6ca5cf53802a2fffd55282ef011341023637 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 15:29:23 +0200 Subject: [PATCH 105/129] collect anatomy objec plugin is using new Anatomy import --- openpype/plugins/publish/collect_anatomy_object.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_object.py b/openpype/plugins/publish/collect_anatomy_object.py index 2c87918728..b1415098b6 100644 --- a/openpype/plugins/publish/collect_anatomy_object.py +++ b/openpype/plugins/publish/collect_anatomy_object.py @@ -4,11 +4,11 @@ Requires: os.environ -> AVALON_PROJECT Provides: - context -> anatomy (pype.api.Anatomy) + context -> anatomy (openpype.pipeline.anatomy.Anatomy) """ import os -from openpype.api import Anatomy import pyblish.api +from openpype.pipeline import Anatomy class CollectAnatomyObject(pyblish.api.ContextPlugin): From 7bcc7277c394a408ad289a90c131fe8e807eff2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 15:29:59 +0200 Subject: [PATCH 106/129] ftrack is using new source of anatomy --- .../ftrack/event_handlers_server/event_user_assigment.py | 4 ++-- .../ftrack/event_handlers_user/action_create_folders.py | 2 +- .../ftrack/event_handlers_user/action_delete_old_versions.py | 3 +-- .../modules/ftrack/event_handlers_user/action_delivery.py | 3 ++- .../ftrack/event_handlers_user/action_fill_workfile_attr.py | 4 ++-- openpype/modules/ftrack/event_handlers_user/action_rv.py | 2 +- .../event_handlers_user/action_store_thumbnails_to_avalon.py | 3 +-- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py b/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py index 82b79e986b..88d252e8cf 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py +++ b/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py @@ -2,11 +2,11 @@ import re import subprocess from openpype.client import get_asset_by_id, get_asset_by_name +from openpype.settings import get_project_settings +from openpype.pipeline import Anatomy from openpype_modules.ftrack.lib import BaseEvent from openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY -from openpype.api import Anatomy, get_project_settings - class UserAssigmentEvent(BaseEvent): """ diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py index 81f38e0c39..9806f83773 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py @@ -1,7 +1,7 @@ import os import collections import copy -from openpype.api import Anatomy +from openpype.pipeline import Anatomy from openpype_modules.ftrack.lib import BaseAction, statics_icon diff --git a/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py index 3400c509ab..79d04a7854 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py @@ -11,9 +11,8 @@ from openpype.client import ( get_versions, get_representations ) -from openpype.api import Anatomy from openpype.lib import StringTemplate, TemplateUnsolved -from openpype.pipeline import AvalonMongoDB +from openpype.pipeline import AvalonMongoDB, Anatomy from openpype_modules.ftrack.lib import BaseAction, statics_icon diff --git a/openpype/modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py index 4b799b092b..ad82af39a3 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_delivery.py +++ b/openpype/modules/ftrack/event_handlers_user/action_delivery.py @@ -10,12 +10,13 @@ from openpype.client import ( get_versions, get_representations ) -from openpype.api import Anatomy, config +from openpype.pipeline import Anatomy from openpype_modules.ftrack.lib import BaseAction, statics_icon from openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from openpype_modules.ftrack.lib.custom_attributes import ( query_custom_attributes ) +from openpype.lib import config from openpype.lib.delivery import ( path_from_representation, get_format_dict, diff --git a/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py b/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py index d30c41a749..d91649d7ba 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py +++ b/openpype/modules/ftrack/event_handlers_user/action_fill_workfile_attr.py @@ -11,13 +11,13 @@ from openpype.client import ( get_project, get_assets, ) -from openpype.api import get_project_settings +from openpype.settings import get_project_settings from openpype.lib import ( get_workfile_template_key, get_workdir_data, - Anatomy, StringTemplate, ) +from openpype.pipeline import Anatomy from openpype_modules.ftrack.lib import BaseAction, statics_icon from openpype_modules.ftrack.lib.avalon_sync import create_chunks diff --git a/openpype/modules/ftrack/event_handlers_user/action_rv.py b/openpype/modules/ftrack/event_handlers_user/action_rv.py index 2480ea7f95..d05f0c47f6 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_rv.py +++ b/openpype/modules/ftrack/event_handlers_user/action_rv.py @@ -11,10 +11,10 @@ from openpype.client import ( get_version_by_name, get_representation_by_name ) -from openpype.api import Anatomy from openpype.pipeline import ( get_representation_path, AvalonMongoDB, + Anatomy, ) from openpype_modules.ftrack.lib import BaseAction, statics_icon diff --git a/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py b/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py index d655dddcaf..8748f426bd 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py +++ b/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py @@ -14,8 +14,7 @@ from openpype.client import ( get_representations ) from openpype_modules.ftrack.lib import BaseAction, statics_icon -from openpype.api import Anatomy -from openpype.pipeline import AvalonMongoDB +from openpype.pipeline import AvalonMongoDB, Anatomy from openpype_modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY From b4daf3bf769df737d31246fe3adaefeedb48b5c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 15:32:48 +0200 Subject: [PATCH 107/129] use new anatomy source in hosts --- openpype/hooks/pre_global_host_data.py | 3 +-- openpype/hosts/hiero/api/lib.py | 5 +++-- .../vendor/husdoutputprocessors/avalon_uri_processor.py | 3 +-- openpype/hosts/maya/api/plugin.py | 2 +- openpype/hosts/nuke/api/lib.py | 8 +++++--- openpype/hosts/tvpaint/plugins/load/load_workfile.py | 2 +- openpype/hosts/unreal/api/rendering.py | 2 +- .../unreal/plugins/publish/collect_render_instances.py | 2 +- .../deadline/plugins/publish/submit_publish_job.py | 2 +- 9 files changed, 15 insertions(+), 14 deletions(-) diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py index ea5e290d6f..6577e37cbe 100644 --- a/openpype/hooks/pre_global_host_data.py +++ b/openpype/hooks/pre_global_host_data.py @@ -1,11 +1,10 @@ -from openpype.api import Anatomy from openpype.lib import ( PreLaunchHook, EnvironmentPrepData, prepare_app_environments, prepare_context_environments ) -from openpype.pipeline import AvalonMongoDB +from openpype.pipeline import AvalonMongoDB, Anatomy class GlobalHostDataHook(PreLaunchHook): diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 8c8c31bc4c..2f66f3ddd7 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -19,8 +19,9 @@ from openpype.client import ( get_last_versions, get_representations, ) -from openpype.pipeline import legacy_io -from openpype.api import (Logger, Anatomy, get_anatomy_settings) +from openpype.settings import get_anatomy_settings +from openpype.pipeline import legacy_io, Anatomy +from openpype.api import Logger from . import tags try: diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py index 202287f1c3..d7d1c79d73 100644 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py +++ b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py @@ -5,8 +5,7 @@ import husdoutputprocessors.base as base import colorbleed.usdlib as usdlib from openpype.client import get_asset_by_name -from openpype.api import Anatomy -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, Anatomy class AvalonURIOutputProcessor(base.OutputProcessorBase): diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index f05893a7b4..9280805945 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -9,8 +9,8 @@ from openpype.pipeline import ( LoaderPlugin, get_representation_path, AVALON_CONTAINER_ID, + Anatomy, ) -from openpype.api import Anatomy from openpype.settings import get_project_settings from .pipeline import containerise from . import lib diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 45a1e72703..f565ec8546 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -20,21 +20,23 @@ from openpype.client import ( ) from openpype.api import ( Logger, - Anatomy, BuildWorkfile, get_version_from_path, - get_anatomy_settings, get_workdir_data, get_asset, get_current_project_settings, ) from openpype.tools.utils import host_tools from openpype.lib.path_tools import HostDirmap -from openpype.settings import get_project_settings +from openpype.settings import ( + get_project_settings, + get_anatomy_settings, +) from openpype.modules import ModulesManager from openpype.pipeline import ( discover_legacy_creator_plugins, legacy_io, + Anatomy, ) from . import gizmo_menu diff --git a/openpype/hosts/tvpaint/plugins/load/load_workfile.py b/openpype/hosts/tvpaint/plugins/load/load_workfile.py index 462f12abf0..c6dc765a27 100644 --- a/openpype/hosts/tvpaint/plugins/load/load_workfile.py +++ b/openpype/hosts/tvpaint/plugins/load/load_workfile.py @@ -10,8 +10,8 @@ from openpype.lib import ( from openpype.pipeline import ( registered_host, legacy_io, + Anatomy, ) -from openpype.api import Anatomy from openpype.hosts.tvpaint.api import lib, pipeline, plugin diff --git a/openpype/hosts/unreal/api/rendering.py b/openpype/hosts/unreal/api/rendering.py index b2732506fc..29e4747f6e 100644 --- a/openpype/hosts/unreal/api/rendering.py +++ b/openpype/hosts/unreal/api/rendering.py @@ -2,7 +2,7 @@ import os import unreal -from openpype.api import Anatomy +from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline diff --git a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py index 9fb45ea7a7..cb28f4bf60 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_render_instances.py @@ -3,7 +3,7 @@ from pathlib import Path import unreal -from openpype.api import Anatomy +from openpype.pipeline import Anatomy from openpype.hosts.unreal.api import pipeline import pyblish.api diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index b54b00d099..9dd1428a63 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -1045,7 +1045,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): get publish_path Args: - anatomy (pype.lib.anatomy.Anatomy): + anatomy (openpype.pipeline.anatomy.Anatomy): template_data (dict): pre-calculated collected data for process asset (string): asset name subset (string): subset name (actually group name of subset) From 19a5ed7be3967bd3ea25167c11b802275e20ac5b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 29 Jun 2022 17:22:21 +0200 Subject: [PATCH 108/129] Refactor `iter_visible_in_frame_range` -> `iter_visible_nodes_in_range` --- openpype/hosts/maya/api/lib.py | 2 +- openpype/hosts/maya/plugins/publish/extract_animation.py | 4 ++-- openpype/hosts/maya/plugins/publish/extract_pointcache.py | 4 ++-- openpype/hosts/maya/plugins/publish/validate_visible_only.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 43f738bfc7..34340a13a5 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3224,7 +3224,7 @@ def maintained_time(): cmds.currentTime(ct, edit=True) -def iter_visible_in_frame_range(nodes, start, end): +def iter_visible_nodes_in_range(nodes, start, end): """Yield nodes that are visible in start-end frame range. - Ignores intermediateObjects completely. diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index b0beb5968e..8ed2d8d7a3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_animation.py @@ -7,7 +7,7 @@ from openpype.hosts.maya.api.lib import ( extract_alembic, suspended_refresh, maintained_selection, - iter_visible_in_frame_range + iter_visible_nodes_in_range ) @@ -83,7 +83,7 @@ class ExtractAnimation(openpype.api.Extractor): # flag does not filter out those that are only hidden on some # frames as it counts "animated" or "connected" visibilities as # if it's always visible. - nodes = list(iter_visible_in_frame_range(nodes, + nodes = list(iter_visible_nodes_in_range(nodes, start=start, end=end)) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 7aa3aaee2a..775b5e9939 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -7,7 +7,7 @@ from openpype.hosts.maya.api.lib import ( extract_alembic, suspended_refresh, maintained_selection, - iter_visible_in_frame_range + iter_visible_nodes_in_range ) @@ -86,7 +86,7 @@ class ExtractAlembic(openpype.api.Extractor): # flag does not filter out those that are only hidden on some # frames as it counts "animated" or "connected" visibilities as # if it's always visible. - nodes = list(iter_visible_in_frame_range(nodes, + nodes = list(iter_visible_nodes_in_range(nodes, start=start, end=end)) diff --git a/openpype/hosts/maya/plugins/publish/validate_visible_only.py b/openpype/hosts/maya/plugins/publish/validate_visible_only.py index 2a10171113..59a7f976ab 100644 --- a/openpype/hosts/maya/plugins/publish/validate_visible_only.py +++ b/openpype/hosts/maya/plugins/publish/validate_visible_only.py @@ -1,7 +1,7 @@ import pyblish.api import openpype.api -from openpype.hosts.maya.api.lib import iter_visible_in_frame_range +from openpype.hosts.maya.api.lib import iter_visible_nodes_in_range import openpype.hosts.maya.api.action @@ -40,7 +40,7 @@ class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin): nodes = instance[:] start, end = cls.get_frame_range(instance) - if not any(iter_visible_in_frame_range(nodes, start, end)): + if not any(iter_visible_nodes_in_range(nodes, start, end)): # Return the nodes we have considered so the user can identify # them with the select invalid action return nodes From c0b03113a5e126d2afeee462eb8df51e031a2962 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 30 Jun 2022 10:21:25 +0200 Subject: [PATCH 109/129] Added minimal permissions for MongoDB Collected from Discord discussions. --- website/docs/admin_use.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/admin_use.md b/website/docs/admin_use.md index f84905c486..1c4ae9e01c 100644 --- a/website/docs/admin_use.md +++ b/website/docs/admin_use.md @@ -105,6 +105,10 @@ save it in secure way to your systems keyring - on Windows it is **Credential Ma This can be also set beforehand with environment variable `OPENPYPE_MONGO`. If set it takes precedence over the one set in keyring. +:::tip Minimal permissions for DB user +- `readWrite` role to `openpype` and `avalon` databases +- `find` permission on `openpype`, `avalon` and `local` + #### Check for OpenPype version path When connection to MongoDB is made, OpenPype will get various settings from there - one among them is directory location where OpenPype versions are stored. If this directory exists OpenPype tries to From 3b90f7e507afa4f87304db67fe0b77e647283c22 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 11:07:10 +0200 Subject: [PATCH 110/129] log viewer escape characters in log message --- openpype/modules/log_viewer/tray/widgets.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index ed08e62109..c7ac64ab70 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -1,3 +1,4 @@ +import html from Qt import QtCore, QtWidgets import qtawesome from .models import LogModel, LogsFilterProxy @@ -286,7 +287,7 @@ class OutputWidget(QtWidgets.QWidget): if level == "debug": line_f = ( " -" - " {{ {loggerName} }}: [" + " {{ {logger_name} }}: [" " {message}" " ]" ) @@ -299,7 +300,7 @@ class OutputWidget(QtWidgets.QWidget): elif level == "warning": line_f = ( "*** WRN:" - " >>> {{ {loggerName} }}: [" + " >>> {{ {logger_name} }}: [" " {message}" " ]" ) @@ -307,16 +308,25 @@ class OutputWidget(QtWidgets.QWidget): line_f = ( "!!! ERR:" " {timestamp}" - " >>> {{ {loggerName} }}: [" + " >>> {{ {logger_name} }}: [" " {message}" " ]" ) + logger_name = log["loggerName"] + timestamp = "" + if not show_timecode: + timestamp = log["timestamp"] + message = log["message"] exc = log.get("exception") if exc: - log["message"] = exc["message"] + message = exc["message"] - line = line_f.format(**log) + line = line_f.format( + message=html.escape(message), + logger_name=logger_name, + timestamp=timestamp + ) if show_timecode: timestamp = log["timestamp"] From aa741a1148c31a8fc76c6fca90ee885b88e7db33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 11:34:34 +0200 Subject: [PATCH 111/129] use query functions in load logic --- openpype/pipeline/load/utils.py | 149 +++++++++++++------------------- 1 file changed, 62 insertions(+), 87 deletions(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 99e5d11f82..b6e5c38fbe 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -6,9 +6,20 @@ import logging import inspect import numbers -import six -from bson.objectid import ObjectId - +from openpype.client import ( + get_project, + get_assets, + get_subsets, + get_versions, + get_version_by_id, + get_last_version_by_subset_id, + get_hero_version_by_subset_id, + get_version_by_name, + get_representations, + get_representation_by_id, + get_representation_by_name, + get_representation_parents +) from openpype.lib import Anatomy from openpype.pipeline import ( schema, @@ -52,13 +63,10 @@ def get_repres_contexts(representation_ids, dbcon=None): Returns: dict: The full representation context by representation id. - keys are repre_id, value is dictionary with full: - asset_doc - version_doc - subset_doc - repre_doc - + keys are repre_id, value is dictionary with full documents of + asset, subset, version and representation. """ + if not dbcon: dbcon = legacy_io @@ -66,26 +74,18 @@ def get_repres_contexts(representation_ids, dbcon=None): if not representation_ids: return contexts - _representation_ids = [] - for repre_id in representation_ids: - if isinstance(repre_id, six.string_types): - repre_id = ObjectId(repre_id) - _representation_ids.append(repre_id) + project_name = dbcon.active_project() + repre_docs = get_representations(project_name, representation_ids) - repre_docs = dbcon.find({ - "type": "representation", - "_id": {"$in": _representation_ids} - }) repre_docs_by_id = {} version_ids = set() for repre_doc in repre_docs: version_ids.add(repre_doc["parent"]) repre_docs_by_id[repre_doc["_id"]] = repre_doc - version_docs = dbcon.find({ - "type": {"$in": ["version", "hero_version"]}, - "_id": {"$in": list(version_ids)} - }) + version_docs = get_versions( + project_name, verison_ids=version_ids, hero=True + ) version_docs_by_id = {} hero_version_docs = [] @@ -99,10 +99,7 @@ def get_repres_contexts(representation_ids, dbcon=None): subset_ids.add(version_doc["parent"]) if versions_for_hero: - _version_docs = dbcon.find({ - "type": "version", - "_id": {"$in": list(versions_for_hero)} - }) + _version_docs = get_versions(project_name, versions_for_hero) _version_data_by_id = { version_doc["_id"]: version_doc["data"] for version_doc in _version_docs @@ -114,26 +111,20 @@ def get_repres_contexts(representation_ids, dbcon=None): version_data = copy.deepcopy(_version_data_by_id[version_id]) version_docs_by_id[hero_version_id]["data"] = version_data - subset_docs = dbcon.find({ - "type": "subset", - "_id": {"$in": list(subset_ids)} - }) + subset_docs = get_subsets(project_name, subset_ids=subset_ids) subset_docs_by_id = {} asset_ids = set() for subset_doc in subset_docs: subset_docs_by_id[subset_doc["_id"]] = subset_doc asset_ids.add(subset_doc["parent"]) - asset_docs = dbcon.find({ - "type": "asset", - "_id": {"$in": list(asset_ids)} - }) + asset_docs = get_assets(project_name, asset_ids) asset_docs_by_id = { asset_doc["_id"]: asset_doc for asset_doc in asset_docs } - project_doc = dbcon.find_one({"type": "project"}) + project_doc = get_project(project_name) for repre_id, repre_doc in repre_docs_by_id.items(): version_doc = version_docs_by_id[repre_doc["parent"]] @@ -173,32 +164,21 @@ def get_subset_contexts(subset_ids, dbcon=None): if not subset_ids: return contexts - _subset_ids = set() - for subset_id in subset_ids: - if isinstance(subset_id, six.string_types): - subset_id = ObjectId(subset_id) - _subset_ids.add(subset_id) - - subset_docs = dbcon.find({ - "type": "subset", - "_id": {"$in": list(_subset_ids)} - }) + project_name = legacy_io.active_project() + subset_docs = get_subsets(project_name, subset_ids) subset_docs_by_id = {} asset_ids = set() for subset_doc in subset_docs: subset_docs_by_id[subset_doc["_id"]] = subset_doc asset_ids.add(subset_doc["parent"]) - asset_docs = dbcon.find({ - "type": "asset", - "_id": {"$in": list(asset_ids)} - }) + asset_docs = get_assets(project_name, asset_ids) asset_docs_by_id = { asset_doc["_id"]: asset_doc for asset_doc in asset_docs } - project_doc = dbcon.find_one({"type": "project"}) + project_doc = get_project(project_name) for subset_id, subset_doc in subset_docs_by_id.items(): asset_doc = asset_docs_by_id[subset_doc["parent"]] @@ -224,16 +204,17 @@ def get_representation_context(representation): Returns: dict: The full representation context. - """ assert representation is not None, "This is a bug" - if isinstance(representation, (six.string_types, ObjectId)): - representation = legacy_io.find_one( - {"_id": ObjectId(str(representation))}) + if not isinstance(representation, dict): + representation = get_representation_by_id(representation) - version, subset, asset, project = legacy_io.parenthood(representation) + project_name = legacy_io.active_project() + version, subset, asset, project = get_representation_parents( + project_name, representation + ) assert all([representation, version, subset, asset, project]), ( "This is a bug" @@ -405,42 +386,36 @@ def update_container(container, version=-1): """Update a container""" # Compute the different version from 'representation' - current_representation = legacy_io.find_one({ - "_id": ObjectId(container["representation"]) - }) + project_name = legacy_io.active_project() + current_representation = get_representation_by_id( + project_name, container["representation"] + ) assert current_representation is not None, "This is a bug" - current_version, subset, asset, project = legacy_io.parenthood( - current_representation) - + current_version = get_version_by_id( + project_name, current_representation["_id"], fields=["parent"] + ) if version == -1: - new_version = legacy_io.find_one({ - "type": "version", - "parent": subset["_id"] - }, sort=[("name", -1)]) + new_version = get_last_version_by_subset_id( + project_name, current_version["parent"], fields=["_id"] + ) + + elif isinstance(version, HeroVersionType): + new_version = get_hero_version_by_subset_id( + project_name, current_version["parent"], fields=["_id"] + ) + else: - if isinstance(version, HeroVersionType): - version_query = { - "parent": subset["_id"], - "type": "hero_version" - } - else: - version_query = { - "parent": subset["_id"], - "type": "version", - "name": version - } - new_version = legacy_io.find_one(version_query) + new_version = get_version_by_name( + project_name, version, current_version["parent"], fields=["_id"] + ) assert new_version is not None, "This is a bug" - new_representation = legacy_io.find_one({ - "type": "representation", - "parent": new_version["_id"], - "name": current_representation["name"] - }) - + new_representation = get_representation_by_name( + project_name, current_representation["name"], new_version["_id"] + ) assert new_representation is not None, "Representation wasn't found" path = get_representation_path(new_representation) @@ -482,10 +457,10 @@ def switch_container(container, representation, loader_plugin=None): )) # Get the new representation to switch to - new_representation = legacy_io.find_one({ - "type": "representation", - "_id": representation["_id"], - }) + project_name = legacy_io.active_project() + new_representation = get_representation_by_id( + project_name, representation["_id"] + ) new_context = get_representation_context(new_representation) if not is_compatible_loader(loader_plugin, new_context): From 2527ac03728d8c85abd63248a3db539f8451b0be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 11:47:32 +0200 Subject: [PATCH 112/129] make sure _get_project_connection has project name passed --- openpype/client/entities.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 28cd994254..a58926499f 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -15,12 +15,15 @@ from bson.objectid import ObjectId from openpype.lib.mongo import OpenPypeMongoConnection -def _get_project_connection(project_name=None): +def _get_project_database(): db_name = os.environ.get("AVALON_DB") or "avalon" - mongodb = OpenPypeMongoConnection.get_mongo_client()[db_name] - if project_name: - return mongodb[project_name] - return mongodb + return OpenPypeMongoConnection.get_mongo_client()[db_name] + + +def _get_project_connection(project_name): + if not project_name: + raise ValueError("Invalid project name {}".format(str(project_name))) + return _get_project_database()[project_name] def _prepare_fields(fields, required_fields=None): @@ -55,7 +58,7 @@ def _convert_ids(in_ids): def get_projects(active=True, inactive=False, fields=None): - mongodb = _get_project_connection() + mongodb = _get_project_database() for project_name in mongodb.collection_names(): if project_name in ("system.indexes",): continue From 63f5ea427a2bd41d38d352bb6f5b132a39c348bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 11:50:40 +0200 Subject: [PATCH 113/129] make sure output of get_representations_parents always returns values for passed repre ids --- openpype/client/entities.py | 5 +++-- openpype/hosts/maya/api/setdress.py | 7 ++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index a58926499f..8b0c259817 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1012,8 +1012,10 @@ def get_representations_parents(project_name, representations): versions_by_subset_id = collections.defaultdict(list) subsets_by_subset_id = {} subsets_by_asset_id = collections.defaultdict(list) + output = {} for representation in representations: repre_id = representation["_id"] + output[repre_id] = (None, None, None, None) version_id = representation["parent"] repres_by_version_id[version_id].append(representation) @@ -1043,7 +1045,6 @@ def get_representations_parents(project_name, representations): project = get_project(project_name) - output = {} for version_id, representations in repres_by_version_id.items(): asset = None subset = None @@ -1083,7 +1084,7 @@ def get_representation_parents(project_name, representation): parents_by_repre_id = get_representations_parents( project_name, [representation] ) - return parents_by_repre_id.get(repre_id) + return parents_by_repre_id[repre_id] def get_thumbnail_id_from_source(project_name, src_type, src_id): diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py index bea8f154b1..159bfe9eb3 100644 --- a/openpype/hosts/maya/api/setdress.py +++ b/openpype/hosts/maya/api/setdress.py @@ -296,12 +296,9 @@ def update_package_version(container, version): assert current_representation is not None, "This is a bug" - repre_parents = get_representation_parents( - project_name, current_representation + version_doc, subset_doc, asset_doc, project_doc = ( + get_representation_parents(project_name, current_representation) ) - version_doc = subset_doc = asset_doc = project_doc = None - if repre_parents: - version_doc, subset_doc, asset_doc, project_doc = repre_parents if version == -1: new_version = get_last_version_by_subset_id( From dd1e1960da94ab22b28a68f647a685064a0da479 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 11:54:23 +0200 Subject: [PATCH 114/129] fix few issues --- openpype/pipeline/load/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index b6e5c38fbe..3d2f798769 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -84,7 +84,7 @@ def get_repres_contexts(representation_ids, dbcon=None): repre_docs_by_id[repre_doc["_id"]] = repre_doc version_docs = get_versions( - project_name, verison_ids=version_ids, hero=True + project_name, version_ids, hero=True ) version_docs_by_id = {} @@ -111,7 +111,7 @@ def get_repres_contexts(representation_ids, dbcon=None): version_data = copy.deepcopy(_version_data_by_id[version_id]) version_docs_by_id[hero_version_id]["data"] = version_data - subset_docs = get_subsets(project_name, subset_ids=subset_ids) + subset_docs = get_subsets(project_name, subset_ids) subset_docs_by_id = {} asset_ids = set() for subset_doc in subset_docs: @@ -164,7 +164,7 @@ def get_subset_contexts(subset_ids, dbcon=None): if not subset_ids: return contexts - project_name = legacy_io.active_project() + project_name = dbcon.active_project() subset_docs = get_subsets(project_name, subset_ids) subset_docs_by_id = {} asset_ids = set() From 81e977129022b2db7ca1f5593756fb0add035cfa Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 30 Jun 2022 11:55:25 +0200 Subject: [PATCH 115/129] :construction_worker: clean old files and add version subfolder --- inno_setup.iss | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index ead9907955..fa050ef1d6 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -18,7 +18,8 @@ AppPublisher=Orbi Tools s.r.o AppPublisherURL=http://pype.club AppSupportURL=http://pype.club AppUpdatesURL=http://pype.club -DefaultDirName={autopf}\{#MyAppName} +DefaultDirName={autopf}\{#MyAppName}\{#AppVer} +UsePreviousAppDir=no DisableProgramGroupPage=yes OutputBaseFilename={#MyAppName}-{#AppVer}-install AllowCancelDuringInstall=yes @@ -27,7 +28,7 @@ AllowCancelDuringInstall=yes PrivilegesRequiredOverridesAllowed=dialog SetupIconFile=igniter\openpype.ico OutputDir=build\ -Compression=lzma +Compression=lzma2 SolidCompression=yes WizardStyle=modern @@ -37,6 +38,11 @@ Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +[InstallDelete] +; clean everything in previous installation folder +Type: filesandordirs; Name: "{app}\*" + + [Files] Source: "build\{#build}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files From c473f6b777022692fdae72cd927f01afbd51ba2e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 30 Jun 2022 14:17:57 +0200 Subject: [PATCH 116/129] :bug: fix unicode encoding hash in tile rendering --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 9964e3c646..3707c5709f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -710,7 +710,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): new_payload["JobInfo"].update(tiles_data["JobInfo"]) new_payload["PluginInfo"].update(tiles_data["PluginInfo"]) - job_hash = hashlib.sha256("{}_{}".format(file_index, file)) + self.log.info("hashing {} - {}".format(file_index, file)) + job_hash = hashlib.sha256( + ("{}_{}".format(file_index, file)).encode("utf-8")) frame_jobs[frame] = job_hash.hexdigest() new_payload["JobInfo"]["ExtraInfo0"] = job_hash.hexdigest() new_payload["JobInfo"]["ExtraInfo1"] = file From cda8d670c003a85f324fb3e0ce160b1745c1703e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 30 Jun 2022 15:08:19 +0200 Subject: [PATCH 117/129] Added settings for Harmony ImageSequenceLoader Currently to fix issue with reference representations with wrong names (png_mp4 instead of png), but it could be helpful regardless. --- .../defaults/project_settings/harmony.json | 16 +++++++++++ .../schema_project_harmony.json | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/openpype/settings/defaults/project_settings/harmony.json b/openpype/settings/defaults/project_settings/harmony.json index 1508b02e1b..d843bc8e70 100644 --- a/openpype/settings/defaults/project_settings/harmony.json +++ b/openpype/settings/defaults/project_settings/harmony.json @@ -1,4 +1,20 @@ { + "load": { + "ImageSequenceLoader": { + "family": [ + "shot", + "render", + "image", + "plate", + "reference" + ], + "representations": [ + "jpeg", + "png", + "jpg" + ] + } + }, "publish": { "CollectPalettes": { "allowed_tasks": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index c049ce3084..311f742f81 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -5,6 +5,34 @@ "label": "Harmony", "is_file": true, "children": [ + { + "type": "dict", + "collapsible": true, + "key": "load", + "label": "Loader plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "ImageSequenceLoader", + "label": "Load Image Sequence", + "children": [ + { + "type": "list", + "key": "family", + "label": "Families", + "object_type": "text" + }, + { + "type": "list", + "key": "representations", + "label": "Representations", + "object_type": "text" + } + ] + } + ] + }, { "type": "dict", "collapsible": true, From 752615d0d46992b92270790901cff2acadea701c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Jun 2022 15:14:19 +0200 Subject: [PATCH 118/129] general: add prerender to thumbnail exporter --- openpype/plugins/publish/extract_jpeg_exr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_jpeg_exr.py b/openpype/plugins/publish/extract_jpeg_exr.py index a467728e77..42c4cbe062 100644 --- a/openpype/plugins/publish/extract_jpeg_exr.py +++ b/openpype/plugins/publish/extract_jpeg_exr.py @@ -19,7 +19,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): label = "Extract Thumbnail" order = pyblish.api.ExtractorOrder families = [ - "imagesequence", "render", "render2d", + "imagesequence", "render", "render2d", "prerender", "source", "plate", "take" ] hosts = ["shell", "fusion", "resolve"] From f65bab000f42af9d4d11d0e600ead9dabaa3e5ba Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Jun 2022 15:14:54 +0200 Subject: [PATCH 119/129] Nuke: add prerender.farm family to extract review mov --- .../hosts/nuke/plugins/publish/extract_review_data_mov.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 5ea7c352b9..fc16e189fb 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -104,7 +104,10 @@ class ExtractReviewDataMov(openpype.api.Extractor): self, instance, o_name, o_data["extension"], multiple_presets) - if "render.farm" in families: + if ( + "render.farm" in families or + "prerender.farm" in families + ): if "review" in instance.data["families"]: instance.data["families"].remove("review") From 728bfd6571ead30f3971578f5c4767c85fdde378 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 17:56:30 +0200 Subject: [PATCH 120/129] normalize workdir --- openpype/lib/avalon_context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index a03f066300..5ff3b15119 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -604,7 +604,10 @@ def get_workdir_with_workdir_data( anatomy_filled = anatomy.format(workdir_data) # Output is TemplateResult object which contain useful data - return anatomy_filled[template_key]["folder"] + path = anatomy_filled[template_key]["folder"] + if path: + path = os.path.normpath(path) + return path def get_workdir( From 3bf3c0bab4fb73efa3c736df6890609767163151 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 17:56:54 +0200 Subject: [PATCH 121/129] modified anatomy imports in lib itself --- openpype/lib/applications.py | 7 ++----- openpype/lib/avalon_context.py | 9 ++++++++- openpype/lib/path_tools.py | 2 +- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index a81bdeca0f..d229848645 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -20,10 +20,7 @@ from openpype.settings.constants import ( METADATA_KEYS, M_DYNAMIC_KEY_LABEL ) -from . import ( - PypeLogger, - Anatomy -) +from . import PypeLogger from .profiles_filtering import filter_profiles from .local_settings import get_openpype_username from .avalon_context import ( @@ -1305,7 +1302,7 @@ def get_app_environments_for_context( dict: Environments for passed context and application. """ - from openpype.pipeline import AvalonMongoDB + from openpype.pipeline import AvalonMongoDB, Anatomy # Avalon database connection dbcon = AvalonMongoDB() diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 5ff3b15119..a7078f9eca 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -14,7 +14,6 @@ from openpype.settings import ( get_project_settings, get_system_settings ) -from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event from .path_templates import StringTemplate @@ -593,6 +592,7 @@ def get_workdir_with_workdir_data( )) if not anatomy: + from openpype.pipeline import Anatomy anatomy = Anatomy(project_name) if not template_key: @@ -638,6 +638,7 @@ def get_workdir( TemplateResult: Workdir path. """ if not anatomy: + from openpype.pipeline import Anatomy anatomy = Anatomy(project_doc["name"]) workdir_data = get_workdir_data( @@ -750,6 +751,8 @@ def compute_session_changes( @with_pipeline_io def get_workdir_from_session(session=None, template_key=None): + from openpype.pipeline import Anatomy + if session is None: session = legacy_io.Session project_name = session["AVALON_PROJECT"] @@ -856,6 +859,8 @@ def create_workfile_doc(asset_doc, task_name, filename, workdir, dbcon=None): dbcon (AvalonMongoDB): Optionally enter avalon AvalonMongoDB object and `legacy_io` is used if not entered. """ + from openpype.pipeline import Anatomy + # Use legacy_io if dbcon is not entered if not dbcon: dbcon = legacy_io @@ -1676,6 +1681,7 @@ def _get_task_context_data_for_anatomy( """ if anatomy is None: + from openpype.pipeline import Anatomy anatomy = Anatomy(project_doc["name"]) asset_name = asset_doc["name"] @@ -1744,6 +1750,7 @@ def get_custom_workfile_template_by_context( """ if anatomy is None: + from openpype.pipeline import Anatomy anatomy = Anatomy(project_doc["name"]) # get project, asset, task anatomy context data diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index caad20f4d6..4f28be3302 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -9,7 +9,6 @@ import platform from openpype.client import get_project from openpype.settings import get_project_settings -from .anatomy import Anatomy from .profiles_filtering import filter_profiles log = logging.getLogger(__name__) @@ -227,6 +226,7 @@ def fill_paths(path_list, anatomy): def create_project_folders(basic_paths, project_name): + from openpype.pipeline import Anatomy anatomy = Anatomy(project_name) concat_paths = concatenate_splitted_paths(basic_paths, anatomy) From a2b8df6964010f9d835022ef9c28c74f87dfbfbc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 18:04:45 +0200 Subject: [PATCH 122/129] normalize when workdir is received from session --- openpype/lib/avalon_context.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index a7078f9eca..616460410e 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -768,7 +768,10 @@ def get_workdir_from_session(session=None, template_key=None): host_name, project_name=project_name ) - return anatomy_filled[template_key]["folder"] + path = anatomy_filled[template_key]["folder"] + if path: + path = os.path.normpath(path) + return path @with_pipeline_io From 9d556a9ae8f3f42770c4296cd799a427674db062 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Jun 2022 23:47:27 +0200 Subject: [PATCH 123/129] Match set path logic more to Arnold VDB loader from #3433 --- .../maya/plugins/load/load_vdb_to_redshift.py | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py index 7867f49bd1..c6a69dfe35 100644 --- a/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/openpype/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -84,7 +84,9 @@ class LoadVDBtoRedShift(load.LoaderPlugin): name="{}RVSShape".format(label), parent=root) - self._apply_settings(volume_node, path=self.fname) + self._set_path(volume_node, + path=self.fname, + representation=context["representation"]) nodes = [root, volume_node] self[:] = nodes @@ -96,36 +98,6 @@ class LoadVDBtoRedShift(load.LoaderPlugin): context=context, loader=self.__class__.__name__) - def _apply_settings(self, - grid_node, - path): - """Apply the settings for the VDB path to the VRayVolumeGrid""" - from maya import cmds - - # The path points to a single file. However the vdb files could be - # either just that single file or a sequence in a folder so we check - # whether it's a sequence - folder = os.path.dirname(path) - files = os.listdir(folder) - is_single_file = len(files) == 1 - if is_single_file: - filename = path - else: - # The path points to the publish .vdb sequence filepath so we - # find the first file in there that ends with .vdb - files = sorted(files) - first = next((x for x in files if x.endswith(".vdb")), None) - if first is None: - raise RuntimeError("Couldn't find first .vdb file of " - "sequence in: %s" % path) - filename = os.path.join(path, first) - - # Tell Redshift whether it should load as sequence or single file - cmds.setAttr(grid_node + ".useFrameExtension", not is_single_file) - - # Set file path - cmds.setAttr(grid_node + ".fileName", filename, type="string") - def update(self, container, representation): from maya import cmds @@ -137,7 +109,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin): assert len(grid_nodes) == 1, "This is a bug" # Update the VRayVolumeGrid - self._apply_settings(grid_nodes[0], path=path) + self._set_path(grid_nodes[0], path=path, representation=representation) # Update container representation cmds.setAttr(container["objectName"] + ".representation", @@ -162,3 +134,19 @@ class LoadVDBtoRedShift(load.LoaderPlugin): def switch(self, container, representation): self.update(container, representation) + + @staticmethod + def _set_path(grid_node, + path, + representation): + """Apply the settings for the VDB path to the RedshiftVolumeShape""" + from maya import cmds + + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) + + is_sequence = bool(representation["context"].get("frame")) + cmds.setAttr(grid_node + ".useFrameExtension", is_sequence) + + # Set file path + cmds.setAttr(grid_node + ".fileName", path, type="string") From b35d8886e2b0070abde920cd5a18b8786f7575aa Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Jul 2022 09:16:51 +0200 Subject: [PATCH 124/129] use query functions in avalon rest api calls --- openpype/modules/avalon_apps/rest_api.py | 37 +++++++++--------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/openpype/modules/avalon_apps/rest_api.py b/openpype/modules/avalon_apps/rest_api.py index b35f5bf357..a52ce1b6df 100644 --- a/openpype/modules/avalon_apps/rest_api.py +++ b/openpype/modules/avalon_apps/rest_api.py @@ -5,7 +5,12 @@ from bson.objectid import ObjectId from aiohttp.web_response import Response -from openpype.pipeline import AvalonMongoDB +from openpype.client import ( + get_projects, + get_project, + get_assets, + get_asset_by_name, +) from openpype_modules.webserver.base_routes import RestApiEndpoint @@ -14,19 +19,13 @@ class _RestApiEndpoint(RestApiEndpoint): self.resource = resource super(_RestApiEndpoint, self).__init__() - @property - def dbcon(self): - return self.resource.dbcon - class AvalonProjectsEndpoint(_RestApiEndpoint): async def get(self) -> Response: - output = [] - for project_name in self.dbcon.database.collection_names(): - project_doc = self.dbcon.database[project_name].find_one({ - "type": "project" - }) - output.append(project_doc) + output = [ + project_doc + for project_doc in get_projects() + ] return Response( status=200, body=self.resource.encode(output), @@ -36,9 +35,7 @@ class AvalonProjectsEndpoint(_RestApiEndpoint): class AvalonProjectEndpoint(_RestApiEndpoint): async def get(self, project_name) -> Response: - project_doc = self.dbcon.database[project_name].find_one({ - "type": "project" - }) + project_doc = get_project(project_name) if project_doc: return Response( status=200, @@ -53,9 +50,7 @@ class AvalonProjectEndpoint(_RestApiEndpoint): class AvalonAssetsEndpoint(_RestApiEndpoint): async def get(self, project_name) -> Response: - asset_docs = list(self.dbcon.database[project_name].find({ - "type": "asset" - })) + asset_docs = list(get_assets(project_name)) return Response( status=200, body=self.resource.encode(asset_docs), @@ -65,10 +60,7 @@ class AvalonAssetsEndpoint(_RestApiEndpoint): class AvalonAssetEndpoint(_RestApiEndpoint): async def get(self, project_name, asset_name) -> Response: - asset_doc = self.dbcon.database[project_name].find_one({ - "type": "asset", - "name": asset_name - }) + asset_doc = get_asset_by_name(project_name, asset_name) if asset_doc: return Response( status=200, @@ -88,9 +80,6 @@ class AvalonRestApiResource: self.module = avalon_module self.server_manager = server_manager - self.dbcon = AvalonMongoDB() - self.dbcon.install() - self.prefix = "/avalon" self.endpoint_defs = ( From f18a9833a62a0f1910976e00c36668be58d6ce0a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Jul 2022 09:40:38 +0200 Subject: [PATCH 125/129] use query functions in clockify launcher actions --- .../launcher_actions/ClockifyStart.py | 30 +++++-------- .../clockify/launcher_actions/ClockifySync.py | 44 ++++++++----------- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/openpype/modules/clockify/launcher_actions/ClockifyStart.py b/openpype/modules/clockify/launcher_actions/ClockifyStart.py index 4669f98b01..7663aecc31 100644 --- a/openpype/modules/clockify/launcher_actions/ClockifyStart.py +++ b/openpype/modules/clockify/launcher_actions/ClockifyStart.py @@ -1,16 +1,9 @@ -from openpype.api import Logger -from openpype.pipeline import ( - legacy_io, - LauncherAction, -) +from openpype.client import get_asset_by_name +from openpype.pipeline import LauncherAction from openpype_modules.clockify.clockify_api import ClockifyAPI -log = Logger.get_logger(__name__) - - class ClockifyStart(LauncherAction): - name = "clockify_start_timer" label = "Clockify - Start Timer" icon = "clockify_icon" @@ -24,20 +17,19 @@ class ClockifyStart(LauncherAction): return False def process(self, session, **kwargs): - project_name = session['AVALON_PROJECT'] - asset_name = session['AVALON_ASSET'] - task_name = session['AVALON_TASK'] + project_name = session["AVALON_PROJECT"] + asset_name = session["AVALON_ASSET"] + task_name = session["AVALON_TASK"] description = asset_name - asset = legacy_io.find_one({ - 'type': 'asset', - 'name': asset_name - }) - if asset is not None: - desc_items = asset.get('data', {}).get('parents', []) + asset_doc = get_asset_by_name( + project_name, asset_name, fields=["data.parents"] + ) + if asset_doc is not None: + desc_items = asset_doc.get("data", {}).get("parents", []) desc_items.append(asset_name) desc_items.append(task_name) - description = '/'.join(desc_items) + description = "/".join(desc_items) project_id = self.clockapi.get_project_id(project_name) tag_ids = [] diff --git a/openpype/modules/clockify/launcher_actions/ClockifySync.py b/openpype/modules/clockify/launcher_actions/ClockifySync.py index 356bbd0306..c346a1b4f6 100644 --- a/openpype/modules/clockify/launcher_actions/ClockifySync.py +++ b/openpype/modules/clockify/launcher_actions/ClockifySync.py @@ -1,11 +1,6 @@ +from openpype.client import get_projects, get_project from openpype_modules.clockify.clockify_api import ClockifyAPI -from openpype.api import Logger -from openpype.pipeline import ( - legacy_io, - LauncherAction, -) - -log = Logger.get_logger(__name__) +from openpype.pipeline import LauncherAction class ClockifySync(LauncherAction): @@ -22,39 +17,36 @@ class ClockifySync(LauncherAction): return self.have_permissions def process(self, session, **kwargs): - project_name = session.get('AVALON_PROJECT', None) + project_name = session.get("AVALON_PROJECT") or "" projects_to_sync = [] - if project_name.strip() == '' or project_name is None: - for project in legacy_io.projects(): - projects_to_sync.append(project) + if project_name.strip(): + projects_to_sync = [get_project(project_name)] else: - project = legacy_io.find_one({'type': 'project'}) - projects_to_sync.append(project) + projects_to_sync = get_projects() projects_info = {} for project in projects_to_sync: - task_types = project['config']['tasks'].keys() - projects_info[project['name']] = task_types + task_types = project["config"]["tasks"].keys() + projects_info[project["name"]] = task_types clockify_projects = self.clockapi.get_projects() for project_name, task_types in projects_info.items(): - if project_name not in clockify_projects: - response = self.clockapi.add_project(project_name) - if 'id' not in response: - self.log.error('Project {} can\'t be created'.format( - project_name - )) - continue - project_id = response['id'] - else: - project_id = clockify_projects[project_name] + if project_name in clockify_projects: + continue + + response = self.clockapi.add_project(project_name) + if "id" not in response: + self.log.error("Project {} can't be created".format( + project_name + )) + continue clockify_workspace_tags = self.clockapi.get_tags() for task_type in task_types: if task_type not in clockify_workspace_tags: response = self.clockapi.add_tag(task_type) - if 'id' not in response: + if "id" not in response: self.log.error('Task {} can\'t be created'.format( task_type )) From 9bfff7a2cc67e704045e31a5f041677ea89d2514 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 1 Jul 2022 15:23:25 +0300 Subject: [PATCH 126/129] Fix asset data key. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 5 ++--- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index fbf4035505..4c3224e1b4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -58,8 +58,8 @@ class ExtractPlayblast(openpype.api.Extractor): height_preset = capture_presets["Resolution"]["height"] # Set resolution variables from asset values asset_data = instance.data["assetEntity"]["data"] - asset_width = asset_data.get("width") - asset_height = asset_data.get("height") + asset_width = asset_data.get("resolutionWidth") + asset_height = asset_data.get("resolutionHeight") review_instance_width = instance.data.get("Width") review_instance_height = instance.data.get("Height") preset['camera'] = camera @@ -68,7 +68,6 @@ class ExtractPlayblast(openpype.api.Extractor): # if it is a value other than zero, that value is # used, if not then the asset resolution is # used - if review_instance_width and review_instance_height: preset['width'] = review_instance_width preset['height'] = review_instance_height diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index bc15327aa7..c7d7cf150d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -66,8 +66,8 @@ class ExtractThumbnail(openpype.api.Extractor): height_preset = capture_presets["Resolution"]["height"] # Set resolution variables from asset values asset_data = instance.data["assetEntity"]["data"] - asset_width = asset_data.get("width") - asset_height = asset_data.get("height") + asset_width = asset_data.get("resolutionWidth") + asset_height = asset_data.get("resolutionHeight") review_instance_width = instance.data.get("Width") review_instance_height = instance.data.get("Height") # Tests if project resolution is set, From 7122675350c58d2ec02fe6a9ceab7f4f86a8fcfb Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 2 Jul 2022 04:00:05 +0000 Subject: [PATCH 127/129] [Automated] Bump version --- CHANGELOG.md | 46 +++++++++++++++++++++++++++------------------ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 438a563391..9b5d40a52f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,36 @@ # Changelog -## [3.12.1-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.12.1-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.12.0...HEAD) +### 📖 Documentation + +- Docs: Added minimal permissions for MongoDB [\#3441](https://github.com/pypeclub/OpenPype/pull/3441) + +**🆕 New features** + +- Maya: Add VDB to Arnold loader [\#3433](https://github.com/pypeclub/OpenPype/pull/3433) + **🚀 Enhancements** +- Blender: Bugfix - Set fps properly on open [\#3426](https://github.com/pypeclub/OpenPype/pull/3426) - Blender: pre pyside install for all platforms [\#3400](https://github.com/pypeclub/OpenPype/pull/3400) +**🐛 Bug fixes** + +- Nuke: prerender reviewable fails [\#3450](https://github.com/pypeclub/OpenPype/pull/3450) +- Maya: fix hashing in Python 3 for tile rendering [\#3447](https://github.com/pypeclub/OpenPype/pull/3447) +- LogViewer: Escape html characters in log message [\#3443](https://github.com/pypeclub/OpenPype/pull/3443) +- Nuke: Slate frame is integrated [\#3427](https://github.com/pypeclub/OpenPype/pull/3427) + +**🔀 Refactored code** + +- Clockify: Use query functions in clockify actions [\#3458](https://github.com/pypeclub/OpenPype/pull/3458) +- General: Use query functions in rest api calls [\#3457](https://github.com/pypeclub/OpenPype/pull/3457) +- General: Use Anatomy after move to pipeline [\#3436](https://github.com/pypeclub/OpenPype/pull/3436) +- General: Anatomy moved to pipeline [\#3435](https://github.com/pypeclub/OpenPype/pull/3435) + ## [3.12.0](https://github.com/pypeclub/OpenPype/tree/3.12.0) (2022-06-28) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.12.0-nightly.3...3.12.0) @@ -24,7 +47,6 @@ - General: Add ability to change user value for templates [\#3366](https://github.com/pypeclub/OpenPype/pull/3366) - Hosts: More options for in-host callbacks [\#3357](https://github.com/pypeclub/OpenPype/pull/3357) - Multiverse: expose some settings to GUI [\#3350](https://github.com/pypeclub/OpenPype/pull/3350) -- Maya: Allow more data to be published along camera 🎥 [\#3304](https://github.com/pypeclub/OpenPype/pull/3304) **🐛 Bug fixes** @@ -57,7 +79,6 @@ - AfterEffects: Use client query functions [\#3374](https://github.com/pypeclub/OpenPype/pull/3374) - TVPaint: Use client query functions [\#3340](https://github.com/pypeclub/OpenPype/pull/3340) - Ftrack: Use client query functions [\#3339](https://github.com/pypeclub/OpenPype/pull/3339) -- Webpublisher: Use client query functions [\#3333](https://github.com/pypeclub/OpenPype/pull/3333) - Standalone Publisher: Use client query functions [\#3330](https://github.com/pypeclub/OpenPype/pull/3330) **Merged pull requests:** @@ -93,17 +114,15 @@ - nuke: adding extract thumbnail settings 3.10 [\#3347](https://github.com/pypeclub/OpenPype/pull/3347) - General: Fix last version function [\#3345](https://github.com/pypeclub/OpenPype/pull/3345) - Deadline: added OPENPYPE\_MONGO to filter [\#3336](https://github.com/pypeclub/OpenPype/pull/3336) -- Nuke: fixing farm publishing if review is disabled [\#3306](https://github.com/pypeclub/OpenPype/pull/3306) + +**🔀 Refactored code** + +- Webpublisher: Use client query functions [\#3333](https://github.com/pypeclub/OpenPype/pull/3333) ## [3.11.0](https://github.com/pypeclub/OpenPype/tree/3.11.0) (2022-06-17) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.11.0-nightly.4...3.11.0) -### 📖 Documentation - -- Documentation: Add app key to template documentation [\#3299](https://github.com/pypeclub/OpenPype/pull/3299) -- doc: adding royal render and multiverse to the web site [\#3285](https://github.com/pypeclub/OpenPype/pull/3285) - **🚀 Enhancements** - Settings: Settings can be extracted from UI [\#3323](https://github.com/pypeclub/OpenPype/pull/3323) @@ -111,8 +130,6 @@ - Ftrack: Action to easily create daily review session [\#3310](https://github.com/pypeclub/OpenPype/pull/3310) - TVPaint: Extractor use mark in/out range to render [\#3309](https://github.com/pypeclub/OpenPype/pull/3309) - Ftrack: Delivery action can work on ReviewSessions [\#3307](https://github.com/pypeclub/OpenPype/pull/3307) -- Maya: Look assigner UI improvements [\#3298](https://github.com/pypeclub/OpenPype/pull/3298) -- Ftrack: Action to transfer values of hierarchical attributes [\#3284](https://github.com/pypeclub/OpenPype/pull/3284) **🐛 Bug fixes** @@ -120,17 +137,10 @@ - Houdini: Fix Houdini VDB manage update wrong file attribute name [\#3322](https://github.com/pypeclub/OpenPype/pull/3322) - Nuke: anatomy compatibility issue hacks [\#3321](https://github.com/pypeclub/OpenPype/pull/3321) - hiero: otio p3 compatibility issue - metadata on effect use update 3.11 [\#3314](https://github.com/pypeclub/OpenPype/pull/3314) -- General: Vendorized modules for Python 2 and update poetry lock [\#3305](https://github.com/pypeclub/OpenPype/pull/3305) -- Fix - added local targets to install host [\#3303](https://github.com/pypeclub/OpenPype/pull/3303) -- Settings: Add missing default settings for nuke gizmo [\#3301](https://github.com/pypeclub/OpenPype/pull/3301) -- Maya: Fix swaped width and height in reviews [\#3300](https://github.com/pypeclub/OpenPype/pull/3300) -- Maya: point cache publish handles Maya instances [\#3297](https://github.com/pypeclub/OpenPype/pull/3297) -- Global: extract review slate issues [\#3286](https://github.com/pypeclub/OpenPype/pull/3286) **🔀 Refactored code** - Blender: Use client query functions [\#3331](https://github.com/pypeclub/OpenPype/pull/3331) -- General: Define query functions [\#3288](https://github.com/pypeclub/OpenPype/pull/3288) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 633b0b4f33..92cdcf9fdd 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.12.1-nightly.1" +__version__ = "3.12.1-nightly.2" diff --git a/pyproject.toml b/pyproject.toml index 26a7b4bf1e..401b24243b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.12.1-nightly.1" # OpenPype +version = "3.12.1-nightly.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From ad9b01c0c7fb1c40b6d56b889a6a7d766390502d Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 4 Jul 2022 02:45:58 +0300 Subject: [PATCH 128/129] Alter final logic and change attribute naming to more pythonic convention. --- openpype/hosts/maya/plugins/create/create_review.py | 4 ++-- .../hosts/maya/plugins/publish/collect_review.py | 4 ++-- .../hosts/maya/plugins/publish/extract_playblast.py | 13 +++++++------ .../hosts/maya/plugins/publish/extract_thumbnail.py | 10 +++++----- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index 9f6721762b..ba51ffa009 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -35,8 +35,8 @@ class CreateReview(plugin.Creator): for key, value in animation_data.items(): data[key] = value - data["Width"] = self.Width - data["Height"] = self.Height + data["review_width"] = self.Width + data["review_height"] = self.Height data["isolate"] = self.isolate data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index b0b5ef37e8..eb872c2935 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -71,8 +71,8 @@ class CollectReview(pyblish.api.InstancePlugin): data['handles'] = instance.data.get('handles', None) data['step'] = instance.data['step'] data['fps'] = instance.data['fps'] - data['Width'] = instance.data['Width'] - data['Height'] = instance.data['Height'] + data['review_width'] = instance.data['review_width'] + data['review_height'] = instance.data['review_height'] data["isolate"] = instance.data["isolate"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 4c3224e1b4..0b60e01e45 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -60,8 +60,8 @@ class ExtractPlayblast(openpype.api.Extractor): asset_data = instance.data["assetEntity"]["data"] asset_width = asset_data.get("resolutionWidth") asset_height = asset_data.get("resolutionHeight") - review_instance_width = instance.data.get("Width") - review_instance_height = instance.data.get("Height") + review_instance_width = instance.data.get("review_width") + review_instance_height = instance.data.get("review_height") preset['camera'] = camera # Tests if project resolution is set, @@ -71,13 +71,14 @@ class ExtractPlayblast(openpype.api.Extractor): if review_instance_width and review_instance_height: preset['width'] = review_instance_width preset['height'] = review_instance_height - elif asset_width and asset_height: - preset['width'] = asset_width - preset['height'] = asset_height elif width_preset and height_preset: preset['width'] = width_preset preset['height'] = height_preset - + elif asset_width and asset_height: + preset['width'] = asset_width + preset['height'] = asset_height + + preset['start_frame'] = start preset['end_frame'] = end camera_option = preset.get("camera_option", {}) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index c7d7cf150d..2f7e6c5e05 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -68,8 +68,8 @@ class ExtractThumbnail(openpype.api.Extractor): asset_data = instance.data["assetEntity"]["data"] asset_width = asset_data.get("resolutionWidth") asset_height = asset_data.get("resolutionHeight") - review_instance_width = instance.data.get("Width") - review_instance_height = instance.data.get("Height") + review_instance_width = instance.data.get("review_width") + review_instance_height = instance.data.get("review_height") # Tests if project resolution is set, # if it is a value other than zero, that value is # used, if not then the asset resolution is @@ -77,12 +77,12 @@ class ExtractThumbnail(openpype.api.Extractor): if review_instance_width and review_instance_height: preset['width'] = review_instance_width preset['height'] = review_instance_height - elif asset_width and asset_height: - preset['width'] = asset_width - preset['height'] = asset_height elif width_preset and height_preset: preset['width'] = width_preset preset['height'] = height_preset + elif asset_width and asset_height: + preset['width'] = asset_width + preset['height'] = asset_height stagingDir = self.staging_dir(instance) filename = "{0}".format(instance.name) path = os.path.join(stagingDir, filename) From 8307ea8ea204f30937589a6f7f8bc493081861ab Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 4 Jul 2022 02:47:10 +0300 Subject: [PATCH 129/129] Style fix. --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0b60e01e45..2c6e76e5ad 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -77,8 +77,6 @@ class ExtractPlayblast(openpype.api.Extractor): elif asset_width and asset_height: preset['width'] = asset_width preset['height'] = asset_height - - preset['start_frame'] = start preset['end_frame'] = end camera_option = preset.get("camera_option", {})