From b74774d02fc4cf2efc8fc00c482845ab1b2dbdaa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Apr 2022 18:09:35 +0200 Subject: [PATCH 01/84] Implement `get_visible_in_frame_range` for extracting Alembics --- openpype/hosts/maya/api/lib.py | 204 ++++++++++++++++++ .../maya/plugins/publish/extract_animation.py | 13 +- .../plugins/publish/extract_pointcache.py | 13 +- 3 files changed, 228 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 801cdb16f4..5304112dcf 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3176,3 +3176,207 @@ def parent_nodes(nodes, parent=None): node[0].setParent(node[1]) if delete_parent: pm.delete(parent_node) + + +@contextlib.contextmanager +def maintained_time(): + ct = cmds.currentTime(query=True) + try: + yield + finally: + cmds.currentTime(ct, edit=True) + + +def get_visible_in_frame_range(nodes, start, end): + """Return nodes that are visible in start-end frame range. + + - Ignores intermediateObjects completely. + - Considers animated visibility attributes + upstream visibilities. + + This is optimized for large scenes where some nodes in the parent + hierarchy might have some input connections to the visibilities, + e.g. key, driven keys, connections to other attributes, etc. + + This only does a single time step to `start` if current frame is + not inside frame range since the assumption is made that changing + a frame isn't so slow that it beats querying all visibility + plugs through MDGContext on another frame. + + Args: + nodes (list): List of node names to consider. + start (int): Start frame. + end (int): End frame. + + Returns: + list: List of node names. These will be long full path names so + might have a longer name than the input nodes. + + """ + # States we consider per node + VISIBLE = 1 # always visible + INVISIBLE = 0 # always invisible + ANIMATED = -1 # animated visibility + + # Ensure integers + start = int(start) + end = int(end) + + # Consider only non-intermediate dag nodes and use the "long" names. + nodes = cmds.ls(nodes, long=True, noIntermediate=True, type="dagNode") + if not nodes: + return [] + + with maintained_time(): + # Go to first frame of the range if we current time is outside of + # the queried range. This is to do a single query on which are at + # least visible at a time inside the range, (e.g those that are + # always visible) + current_time = cmds.currentTime(query=True) + if not (start <= current_time <= end): + cmds.currentTime(start) + + visible = cmds.ls(nodes, long=True, visible=True) + if len(visible) == len(nodes) or start == end: + # All are visible on frame one, so they are at least visible once + # inside the frame range. + return visible + + # For the invisible ones check whether its visibility and/or + # any of its parents visibility attributes are animated. If so, it might + # get visible on other frames in the range. + def memodict(f): + """Memoization decorator for a function taking a single argument. + + See: http://code.activestate.com/recipes/ + 578231-probably-the-fastest-memoization-decorator-in-the-/ + """ + + class memodict(dict): + def __missing__(self, key): + ret = self[key] = f(key) + return ret + + return memodict().__getitem__ + + @memodict + def get_state(node): + plug = node + ".visibility" + connections = cmds.listConnections(plug, + source=True, + destination=False) + if connections: + return ANIMATED + else: + return VISIBLE if cmds.getAttr(plug) else INVISIBLE + + visible = set(visible) + invisible = [node for node in nodes if node not in visible] + always_invisible = set() + # Iterate over the nodes by short to long names, so we iterate the highest + # in hierarcy nodes first. So the collected data can be used from the + # cache for parent queries in next iterations. + node_dependencies = dict() + for node in sorted(invisible, key=len): + + state = get_state(node) + if state == INVISIBLE: + always_invisible.add(node) + continue + + # If not always invisible by itself we should go through and check + # the parents to see if any of them are always invisible. For those + # that are "ANIMATED" we consider that this node is dependent on + # that attribute, we store them as dependency. + dependencies = set() + if state == ANIMATED: + dependencies.add(node) + + traversed_parents = list() + for parent in iter_parents(node): + + if not parent: + # Workaround bug in iter_parents + continue + + if parent in always_invisible or get_state(parent) == INVISIBLE: + # When parent is always invisible then consider this parent, + # this node we started from and any of the parents we + # have traversed in-between to be *always invisible* + always_invisible.add(parent) + always_invisible.add(node) + always_invisible.update(traversed_parents) + break + + # If we have traversed the parent before and its visibility + # was dependent on animated visibilities then we can just extend + # its dependencies for to those for this node and break further + # iteration upwards. + parent_dependencies = node_dependencies.get(parent, None) + if parent_dependencies is not None: + dependencies.update(parent_dependencies) + break + + state = get_state(parent) + if state == ANIMATED: + dependencies.add(parent) + + traversed_parents.append(parent) + + if node not in always_invisible and dependencies: + node_dependencies[node] = dependencies + + if not node_dependencies: + return list(visible) + + # Now we only have to check the visibilities for nodes that have animated + # visibility dependencies upstream. The fastest way to check these + # visibility attributes across different frames is with Python api 2.0 + # so we do that. + @memodict + def get_visibility_mplug(node): + """Return api 2.0 MPlug with cached memoize decorator""" + sel = om.MSelectionList() + sel.add(node) + dag = sel.getDagPath(0) + return om.MFnDagNode(dag).findPlug("visibility", True) + + # We skip the first frame as we already used that frame to check for + # overall visibilities. And end+1 to include the end frame. + scene_units = om.MTime.uiUnit() + for frame in range(start + 1, end + 1): + + mtime = om.MTime(frame, unit=scene_units) + context = om.MDGContext(mtime) + + # Build little cache so we don't query the same MPlug's value + # again if it was checked on this frame and also is a dependency + # for another node + frame_visibilities = {} + + for node, dependencies in list(node_dependencies.items()): + + for dependency in dependencies: + + dependency_visible = frame_visibilities.get(dependency, None) + if dependency_visible is None: + mplug = get_visibility_mplug(dependency) + dependency_visible = mplug.asBool(context) + frame_visibilities[dependency] = dependency_visible + + if not dependency_visible: + # One dependency is not visible, thus the + # node is not visible. + break + + else: + # All dependencies are visible. + visible.add(node) + # Remove node with dependencies for next iterations + # because it was visible at least once. + node_dependencies.pop(node) + + # If no more nodes to process break the frame iterations.. + if not node_dependencies: + break + + return list(visible) diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index 8a8bd67cd8..b63cebde04 100644 --- a/openpype/hosts/maya/plugins/publish/extract_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_animation.py @@ -6,7 +6,8 @@ import openpype.api from openpype.hosts.maya.api.lib import ( extract_alembic, suspended_refresh, - maintained_selection + maintained_selection, + get_visible_in_frame_range ) @@ -70,6 +71,16 @@ class ExtractAnimation(openpype.api.Extractor): # Since Maya 2017 alembic supports multiple uv sets - write them. options["writeUVSets"] = True + if instance.data.get("visibleOnly", False): + # If we only want to include nodes that are visible in the frame + # range then we need to do our own check. Alembic's `visibleOnly` + # 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 = get_visible_in_frame_range(nodes, + start=start, + end=end) + with suspended_refresh(): with maintained_selection(): cmds.select(nodes, noExpand=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 60502fdde1..4510943a48 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -6,7 +6,8 @@ import openpype.api from openpype.hosts.maya.api.lib import ( extract_alembic, suspended_refresh, - maintained_selection + maintained_selection, + get_visible_in_frame_range ) @@ -73,6 +74,16 @@ class ExtractAlembic(openpype.api.Extractor): # Since Maya 2017 alembic supports multiple uv sets - write them. options["writeUVSets"] = True + if instance.data.get("visibleOnly", False): + # If we only want to include nodes that are visible in the frame + # range then we need to do our own check. Alembic's `visibleOnly` + # 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 = get_visible_in_frame_range(nodes, + start=start, + end=end) + with suspended_refresh(): with maintained_selection(): cmds.select(nodes, noExpand=True) From 66f8ebbd0f175e275bae7ae4b555f55b7fe12163 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Apr 2022 09:10:03 +0200 Subject: [PATCH 02/84] Use MDGContext as context manager - Functionality added in Maya 2018 - Usage without `MDGContext.makeCurrent()` is deprecated since Maya 2022 --- openpype/hosts/maya/api/lib.py | 51 +++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 5304112dcf..82de105d16 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3340,40 +3340,47 @@ def get_visible_in_frame_range(nodes, start, end): dag = sel.getDagPath(0) return om.MFnDagNode(dag).findPlug("visibility", True) + @contextlib.contextmanager + def dgcontext(mtime): + """MDGContext context manager""" + context = om.MDGContext(mtime) + try: + previous = context.makeCurrent() + yield context + finally: + previous.makeCurrent() + # We skip the first frame as we already used that frame to check for # overall visibilities. And end+1 to include the end frame. scene_units = om.MTime.uiUnit() for frame in range(start + 1, end + 1): - mtime = om.MTime(frame, unit=scene_units) - context = om.MDGContext(mtime) # Build little cache so we don't query the same MPlug's value # again if it was checked on this frame and also is a dependency # for another node frame_visibilities = {} + with dgcontext(mtime) as context: + for node, dependencies in list(node_dependencies.items()): + for dependency in dependencies: + dependency_visible = frame_visibilities.get(dependency, + None) + if dependency_visible is None: + mplug = get_visibility_mplug(dependency) + dependency_visible = mplug.asBool(context) + frame_visibilities[dependency] = dependency_visible - for node, dependencies in list(node_dependencies.items()): + if not dependency_visible: + # One dependency is not visible, thus the + # node is not visible. + break - for dependency in dependencies: - - dependency_visible = frame_visibilities.get(dependency, None) - if dependency_visible is None: - mplug = get_visibility_mplug(dependency) - dependency_visible = mplug.asBool(context) - frame_visibilities[dependency] = dependency_visible - - if not dependency_visible: - # One dependency is not visible, thus the - # node is not visible. - break - - else: - # All dependencies are visible. - visible.add(node) - # Remove node with dependencies for next iterations - # because it was visible at least once. - node_dependencies.pop(node) + else: + # All dependencies are visible. + visible.add(node) + # Remove node with dependencies for next frame iterations + # because it was visible at least once. + node_dependencies.pop(node) # If no more nodes to process break the frame iterations.. if not node_dependencies: From 338bb1560a33582364f3a45724cf2ff19f89a2da Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Apr 2022 09:43:16 +0200 Subject: [PATCH 03/84] Fix bug in `iter_parents` Previously an empty string could be yielded for e.g. `"|cube"` splitting to `["", "cube"]` --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 82de105d16..901b8c4a4c 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -1876,7 +1876,7 @@ def iter_parents(node): """ while True: split = node.rsplit("|", 1) - if len(split) == 1: + if len(split) == 1 or not split[0]: return node = split[0] From b7d52214101960e8e821efdca417306ab99957e8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 26 Apr 2022 09:43:56 +0200 Subject: [PATCH 04/84] Remove redundant workaround --- openpype/hosts/maya/api/lib.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 901b8c4a4c..52e84c00ab 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3294,10 +3294,6 @@ def get_visible_in_frame_range(nodes, start, end): traversed_parents = list() for parent in iter_parents(node): - if not parent: - # Workaround bug in iter_parents - continue - if parent in always_invisible or get_state(parent) == INVISIBLE: # When parent is always invisible then consider this parent, # this node we started from and any of the parents we From 4c259c443e1c9a96c92bf48113eda9d31bff309b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 28 Apr 2022 17:30:21 +0200 Subject: [PATCH 05/84] Improve comments --- openpype/hosts/maya/api/lib.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 52e84c00ab..79f2442d16 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3227,10 +3227,9 @@ def get_visible_in_frame_range(nodes, start, end): return [] with maintained_time(): - # Go to first frame of the range if we current time is outside of - # the queried range. This is to do a single query on which are at - # least visible at a time inside the range, (e.g those that are - # always visible) + # Go to first frame of the range if the current time is outside + # the queried range so can directly query all visible nodes on + # that frame. current_time = cmds.currentTime(query=True) if not (start <= current_time <= end): cmds.currentTime(start) @@ -3272,8 +3271,8 @@ def get_visible_in_frame_range(nodes, start, end): visible = set(visible) invisible = [node for node in nodes if node not in visible] always_invisible = set() - # Iterate over the nodes by short to long names, so we iterate the highest - # in hierarcy nodes first. So the collected data can be used from the + # Iterate over the nodes by short to long names to iterate the highest + # in hierarchy nodes first. So the collected data can be used from the # cache for parent queries in next iterations. node_dependencies = dict() for node in sorted(invisible, key=len): From 83de346d990ac9e86c10cf85548483d5db17e176 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 16 May 2022 14:59:37 +0200 Subject: [PATCH 06/84] 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 07/84] 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 08/84] 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 09/84] 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 10/84] 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 11/84] 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 12/84] 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 13/84] 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 14/84] 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 15/84] 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 d4df16bb87aac5b667bed8b5ecdd9f6afd52f20b Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Fri, 17 Jun 2022 08:08:05 +0300 Subject: [PATCH 16/84] 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 17/84] 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 18/84] 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 19/84] 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 20/84] 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 21/84] 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 6de95ba8064a9ec06757dc8d8fc2b0e15e12498b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Jun 2022 11:04:02 +0200 Subject: [PATCH 22/84] 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 23/84] 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 24/84] 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 25/84] :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 26/84] :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 27/84] 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 28/84] 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 29/84] added some docstrings --- openpype/host/host.py | 95 ++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 2a59daf473..ab75ec5bc3 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -5,6 +5,13 @@ import six class MissingMethodsError(ValueError): + """Exception when host miss some required methods for specific workflow. + + Args: + host (HostBase): Host implementation where are missing methods. + missing_methods (list[str]): List of missing methods. + """ + def __init__(self, host, missing_methods): joined_missing = ", ".join( ['"{}"'.format(item) for item in missing_methods] @@ -53,15 +60,15 @@ class HostBase(object): install_host(host) ``` - # TODOs - - move content of 'install_host' as method of this class - - register host object - - install legacy_io - - install global plugin paths - - store registered plugin paths to this object - - handle current context (project, asset, task) - - this must be done in many separated steps - - have it's object of host tools instead of using globals + Todo: + - move content of 'install_host' as method of this class + - register host object + - install legacy_io + - install global plugin paths + - store registered plugin paths to this object + - handle current context (project, asset, task) + - this must be done in many separated steps + - have it's object of host tools instead of using globals This implementation will probably change over time when more functionality and responsibility will be added. @@ -75,7 +82,7 @@ class HostBase(object): Register DCC callbacks, host specific plugin paths, targets etc. (Part of what 'install' did in 'avalon' concept.) - NOTE: + Note: At this moment global "installation" must happen before host installation. Because of this current limitation it is recommended to implement 'install' method which is triggered after global @@ -127,10 +134,10 @@ class HostBase(object): Should return current context title if possible. - NOTE: This method is used only for UI purposes so it is possible to - return some logical title for contextless cases. - - Is not meant for "Context menu" label. + Note: + This method is used only for UI purposes so it is possible to + return some logical title for contextless cases. + Is not meant for "Context menu" label. Returns: str: Context title. @@ -159,6 +166,9 @@ class HostBase(object): This is DCC specific. Some may not allow to implement this ability that is reason why default implementation is empty context manager. + + Yields: + None: Yield when is ready to restore selected at the end. """ try: @@ -173,11 +183,11 @@ class ILoadHost: The load plugins can do referencing even without implementation of methods here, but switch and removement of containers would not be possible. - QUESTIONS - - Is list container dependency of host or load plugins? - - Should this be directly in HostBase? - - how to find out if referencing is available? - - do we need to know that? + Questions: + - Is list container dependency of host or load plugins? + - Should this be directly in HostBase? + - how to find out if referencing is available? + - do we need to know that? """ @staticmethod @@ -188,6 +198,9 @@ class ILoadHost: loading. Checks only existence of methods. Args: + HostBase: Object of host where to look for required methods. + + Returns: list[str]: Missing method implementations for loading workflow. """ @@ -202,6 +215,9 @@ class ILoadHost: def validate_load_methods(host): """Validate implemented methods of host for load workflow. + Args: + HostBase: Object of host to validate. + Raises: MissingMethodsError: If there are missing methods on host implementation. @@ -216,7 +232,7 @@ class ILoadHost: This can be implemented in hosts where referencing can be used. - TODO: + Todo: Rename function to something more self explanatory. Suggestion: 'get_referenced_containers' @@ -242,6 +258,9 @@ class IWorkfileHost: Method is used for validation of implemented functions related to workfiles. Checks only existence of methods. + Args: + HostBase: Object of host where to look for required methods. + Returns: list[str]: Missing method implementations for workfiles workflow. """ @@ -264,6 +283,9 @@ class IWorkfileHost: def validate_workfile_methods(host): """Validate implemented methods of host for workfiles workflow. + Args: + HostBase: Object of host to validate. + Raises: MissingMethodsError: If there are missing methods on host implementation. @@ -276,9 +298,10 @@ class IWorkfileHost: def file_extensions(self): """Extensions that can be used as save. - QUESTION: This could potentially use 'HostDefinition'. + Questions: + This could potentially use 'HostDefinition'. - TODO: + Todo: Rename to 'get_workfile_extensions'. """ @@ -288,7 +311,7 @@ class IWorkfileHost: def save_file(self, dst_path=None): """Save currently opened scene. - TODO: + Todo: Rename to 'save_current_workfile'. Args: @@ -302,7 +325,7 @@ class IWorkfileHost: def open_file(self, filepath): """Open passed filepath in the host. - TODO: + Todo: Rename to 'open_workfile'. Args: @@ -315,7 +338,7 @@ class IWorkfileHost: def current_file(self): """Retreive path to current opened file. - TODO: + Todo: Rename to 'get_current_workfile'. Returns: @@ -342,16 +365,16 @@ class IWorkfileHost: def work_root(self, session): """Modify workdir per host. - WARNING: - We must handle this modification with more sofisticated way because - this can't be called out of DCC so opening of last workfile - (calculated before DCC is launched) is complicated. Also breaking - defined work template is not a good idea. - Only place where it's really used and can make sense is Maya. There - workspace.mel can modify subfolders where to look for maya files. - Default implementation keeps workdir untouched. + Warnings: + We must handle this modification with more sofisticated way because + this can't be called out of DCC so opening of last workfile + (calculated before DCC is launched) is complicated. Also breaking + defined work template is not a good idea. + Only place where it's really used and can make sense is Maya. There + workspace.mel can modify subfolders where to look for maya files. + Args: session (dict): Session context data. @@ -381,6 +404,9 @@ class INewPublisher: new publish creation. Checks only existence of methods. Args: + HostBase: Object of host where to look for required methods. + + Returns: list[str]: Missing method implementations for new publsher workflow. """ @@ -399,6 +425,9 @@ class INewPublisher: def validate_publish_methods(host): """Validate implemented methods of host for create-publish workflow. + Args: + HostBase: Object of host to validate. + Raises: MissingMethodsError: If there are missing methods on host implementation. From e36c80fd09adea0322bc03c3c4da1764cc620d62 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 15:41:41 +0200 Subject: [PATCH 30/84] added type hints --- openpype/host/host.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/host/host.py b/openpype/host/host.py index ab75ec5bc3..676f0e6771 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -3,6 +3,9 @@ import contextlib from abc import ABCMeta, abstractproperty, abstractmethod import six +# NOTE can't import 'typing' because of issues in Maya 2020 +# - shiboken crashes on 'typing' module import + class MissingMethodsError(ValueError): """Exception when host miss some required methods for specific workflow. @@ -77,6 +80,7 @@ class HostBase(object): _log = None def __init__(self): + # type: () -> None """Initialization of host. Register DCC callbacks, host specific plugin paths, targets etc. @@ -93,17 +97,20 @@ class HostBase(object): @property def log(self): + # type: () -> logging.Logger if self._log is None: self._log = logging.getLogger(self.__class__.__name__) return self._log @abstractproperty def name(self): + # type: () -> str """Host implementation name.""" pass def get_current_context(self): + # type: () -> Mapping[str, Union[str, None]] """Get current context information. This method should be used to get current context of host. Usage of @@ -130,6 +137,7 @@ class HostBase(object): } def get_context_title(self): + # type: () -> Union[str, None] """Context title shown for UI purposes. Should return current context title if possible. @@ -162,6 +170,7 @@ class HostBase(object): @contextlib.contextmanager def maintained_selection(self): + # type: () -> None """Some functionlity will happen but selection should stay same. This is DCC specific. Some may not allow to implement this ability @@ -192,6 +201,7 @@ class ILoadHost: @staticmethod def get_missing_load_methods(host): + # type: (HostBase) -> List[str] """Look for missing methods on host implementation. Method is used for validation of implemented functions related to @@ -213,6 +223,7 @@ class ILoadHost: @staticmethod def validate_load_methods(host): + # type: (HostBase) -> None """Validate implemented methods of host for load workflow. Args: @@ -228,6 +239,7 @@ class ILoadHost: @abstractmethod def ls(self): + # type: (HostBase) -> List[Mapping[str, Any]] """Retreive referenced containers from scene. This can be implemented in hosts where referencing can be used. @@ -253,6 +265,7 @@ class IWorkfileHost: @staticmethod def get_missing_workfile_methods(host): + # type: (HostBase) -> List[str] """Look for missing methods on host implementation. Method is used for validation of implemented functions related to @@ -281,6 +294,7 @@ class IWorkfileHost: @staticmethod def validate_workfile_methods(host): + # type: (HostBase) -> None """Validate implemented methods of host for workfiles workflow. Args: @@ -296,6 +310,7 @@ class IWorkfileHost: @abstractmethod def file_extensions(self): + # type: () -> List[str] """Extensions that can be used as save. Questions: @@ -309,6 +324,7 @@ class IWorkfileHost: @abstractmethod def save_file(self, dst_path=None): + # type: (Optional[str]) -> None """Save currently opened scene. Todo: @@ -323,6 +339,7 @@ class IWorkfileHost: @abstractmethod def open_file(self, filepath): + # type: (str) -> None """Open passed filepath in the host. Todo: @@ -336,6 +353,7 @@ class IWorkfileHost: @abstractmethod def current_file(self): + # type: () -> Union[str, None] """Retreive path to current opened file. Todo: @@ -349,6 +367,7 @@ class IWorkfileHost: return None def has_unsaved_changes(self): + # type: () -> Union[bool, None] """Currently opened scene is saved. Not all hosts can know if current scene is saved because the API of @@ -363,6 +382,7 @@ class IWorkfileHost: return None def work_root(self, session): + # type: (Mapping[str, str]) -> str """Modify workdir per host. Default implementation keeps workdir untouched. @@ -398,6 +418,7 @@ class INewPublisher: @staticmethod def get_missing_publish_methods(host): + # type: (HostBase) -> List[str] """Look for missing methods on host implementation. Method is used for validation of implemented functions related to @@ -423,6 +444,7 @@ class INewPublisher: @staticmethod def validate_publish_methods(host): + # type: (HostBase) -> None """Validate implemented methods of host for create-publish workflow. Args: @@ -438,6 +460,7 @@ class INewPublisher: @abstractmethod def get_context_data(self): + # type: () -> Mapping[str, Any] """Get global data related to creation-publishing from workfile. These data are not related to any created instance but to whole @@ -455,6 +478,7 @@ class INewPublisher: @abstractmethod def update_context_data(self, data, changes): + # type: (Mapping[str, Any], Mapping[str, Any]) -> None """Store global context data to workfile. Called when some values in context data has changed. From bf592641e870556c474da160b1e107a3377388a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Jun 2022 17:13:29 +0200 Subject: [PATCH 31/84] 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 32/84] 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 33/84] 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 34/84] :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 35/84] 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 36/84] 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 37/84] 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 38/84] 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 39/84] 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 40/84] :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 41/84] 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 42/84] 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 46bfbd28506be577532cb68f319a3051e63957ee Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Mon, 27 Jun 2022 06:13:35 +0300 Subject: [PATCH 43/84] 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 6af7f906e5b3cde5d76aac36676e2a3ee3f18ac6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 27 Jun 2022 13:19:15 +0200 Subject: [PATCH 44/84] 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 45/84] 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 46/84] 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 409bf1b6e51c9ea6ab1fe6be1aca58dda8aa0bb4 Mon Sep 17 00:00:00 2001 From: Allan Ihsan Date: Tue, 28 Jun 2022 05:24:42 +0300 Subject: [PATCH 47/84] 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 48/84] 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 3b8b479712af6e95545539921f0439de547c460b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 28 Jun 2022 09:07:28 +0200 Subject: [PATCH 49/84] Skip extraction when no visible nodes found in frame range --- openpype/hosts/maya/plugins/publish/extract_animation.py | 7 +++++++ openpype/hosts/maya/plugins/publish/extract_pointcache.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index b04e2bf0c4..4b650b4b26 100644 --- a/openpype/hosts/maya/plugins/publish/extract_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_animation.py @@ -86,6 +86,13 @@ class ExtractAnimation(openpype.api.Extractor): nodes = get_visible_in_frame_range(nodes, start=start, end=end) + if not nodes: + self.log.warning( + "No visible nodes found in frame range {}-{}. " + "Skipping extraction because `visibleOnly` is enabled on " + "the instance.".format(start, end) + ) + return with suspended_refresh(): with maintained_selection(): diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index f582a106d9..768ee6da3d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -89,6 +89,13 @@ class ExtractAlembic(openpype.api.Extractor): nodes = get_visible_in_frame_range(nodes, start=start, end=end) + if not nodes: + self.log.warning( + "No visible nodes found in frame range {}-{}. " + "Skipping extraction because `visibleOnly` is enabled on " + "the instance.".format(start, end) + ) + return with suspended_refresh(): with maintained_selection(): From f846ef45d473ba4849f4bb0ed73fc7f9fcb1a546 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Jun 2022 10:52:12 +0200 Subject: [PATCH 50/84] 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 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 51/84] 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 52/84] 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 53/84] 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 54/84] 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 55/84] 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 56/84] 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 ee273892e871b57607a8f8ad1792d3c5ef5ad95b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 28 Jun 2022 21:10:11 +0200 Subject: [PATCH 57/84] Add alembic visible only validator --- openpype/hosts/maya/api/lib.py | 4 +- .../plugins/publish/validate_visible_only.py | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/validate_visible_only.py diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b5bdca456e..ab9c06280f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3241,8 +3241,8 @@ def get_visible_in_frame_range(nodes, start, end): Args: nodes (list): List of node names to consider. - start (int): Start frame. - end (int): End frame. + start (int, float): Start frame. + end (int, float): End frame. Returns: list: List of node names. These will be long full path names so diff --git a/openpype/hosts/maya/plugins/publish/validate_visible_only.py b/openpype/hosts/maya/plugins/publish/validate_visible_only.py new file mode 100644 index 0000000000..9094a421c6 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_visible_only.py @@ -0,0 +1,53 @@ +import pyblish.api + +import openpype.api +from openpype.hosts.maya.api.lib import get_visible_in_frame_range +import openpype.hosts.maya.api.action + + +class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin): + """Validates at least a single node is visible in frame range. + + This validation only validates if the `visibleOnly` flag is enabled + on the instance - otherwise the validation is skipped. + + """ + order = openpype.api.ValidateContentsOrder + 0.05 + label = "Alembic Visible Only" + hosts = ["maya"] + families = ["pointcache", "animation"] + actions = [openpype.hosts.maya.api.action.SelectInvalidAction] + + def process(self, instance): + + if not instance.data.get("visibleOnly", False): + self.log.debug("Visible only is disabled. Validation skipped..") + return + + invalid = self.get_invalid(instance) + if invalid: + start, end = self.get_frame_range(instance) + raise RuntimeError("No visible nodes found in " + "frame range {}-{}.".format(start, end)) + + @classmethod + def get_invalid(cls, instance): + + if instance.data["family"] == "animation": + # Special behavior to use the nodes in out_SET + nodes = instance.data["out_hierarchy"] + else: + nodes = instance[:] + + start, end = cls.get_frame_range(instance) + visible = get_visible_in_frame_range(nodes, start, end) + + if not visible: + # Return the nodes we have considered so the user can identify + # them with the select invalid action + return nodes + + @staticmethod + def get_frame_range(instance): + data = instance.data + return data["frameStartHandle"], data["frameEndHandle"] From 96150eba27fc64e519b7c1756e6b0f36f886746a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 28 Jun 2022 21:10:45 +0200 Subject: [PATCH 58/84] Remove no visible nodes warning in extractors since validator catches those cases --- openpype/hosts/maya/plugins/publish/extract_animation.py | 7 ------- openpype/hosts/maya/plugins/publish/extract_pointcache.py | 7 ------- 2 files changed, 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index 4b650b4b26..b04e2bf0c4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_animation.py @@ -86,13 +86,6 @@ class ExtractAnimation(openpype.api.Extractor): nodes = get_visible_in_frame_range(nodes, start=start, end=end) - if not nodes: - self.log.warning( - "No visible nodes found in frame range {}-{}. " - "Skipping extraction because `visibleOnly` is enabled on " - "the instance.".format(start, end) - ) - return with suspended_refresh(): with maintained_selection(): diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 768ee6da3d..f582a106d9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -89,13 +89,6 @@ class ExtractAlembic(openpype.api.Extractor): nodes = get_visible_in_frame_range(nodes, start=start, end=end) - if not nodes: - self.log.warning( - "No visible nodes found in frame range {}-{}. " - "Skipping extraction because `visibleOnly` is enabled on " - "the instance.".format(start, end) - ) - return with suspended_refresh(): with maintained_selection(): From a29d1d493243915db96d6d09a40471c3177b9558 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 28 Jun 2022 21:25:17 +0200 Subject: [PATCH 59/84] Use iterator to speed up visible only validation for most cases --- openpype/hosts/maya/api/lib.py | 15 +++++++-------- .../maya/plugins/publish/extract_animation.py | 8 ++++---- .../maya/plugins/publish/extract_pointcache.py | 8 ++++---- .../maya/plugins/publish/validate_visible_only.py | 6 ++---- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index ab9c06280f..637f9e951b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3224,8 +3224,8 @@ def maintained_time(): cmds.currentTime(ct, edit=True) -def get_visible_in_frame_range(nodes, start, end): - """Return nodes that are visible in start-end frame range. +def iter_visible_in_frame_range(nodes, start, end): + """Yield nodes that are visible in start-end frame range. - Ignores intermediateObjects completely. - Considers animated visibility attributes + upstream visibilities. @@ -3261,7 +3261,7 @@ def get_visible_in_frame_range(nodes, start, end): # Consider only non-intermediate dag nodes and use the "long" names. nodes = cmds.ls(nodes, long=True, noIntermediate=True, type="dagNode") if not nodes: - return [] + return with maintained_time(): # Go to first frame of the range if the current time is outside @@ -3275,7 +3275,8 @@ def get_visible_in_frame_range(nodes, start, end): if len(visible) == len(nodes) or start == end: # All are visible on frame one, so they are at least visible once # inside the frame range. - return visible + for node in visible: + yield node # For the invisible ones check whether its visibility and/or # any of its parents visibility attributes are animated. If so, it might @@ -3358,7 +3359,7 @@ def get_visible_in_frame_range(nodes, start, end): node_dependencies[node] = dependencies if not node_dependencies: - return list(visible) + return # Now we only have to check the visibilities for nodes that have animated # visibility dependencies upstream. The fastest way to check these @@ -3409,7 +3410,7 @@ def get_visible_in_frame_range(nodes, start, end): else: # All dependencies are visible. - visible.add(node) + yield node # Remove node with dependencies for next frame iterations # because it was visible at least once. node_dependencies.pop(node) @@ -3417,5 +3418,3 @@ def get_visible_in_frame_range(nodes, start, end): # If no more nodes to process break the frame iterations.. if not node_dependencies: break - - return list(visible) diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index b04e2bf0c4..b0beb5968e 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, - get_visible_in_frame_range + iter_visible_in_frame_range ) @@ -83,9 +83,9 @@ 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 = get_visible_in_frame_range(nodes, - start=start, - end=end) + nodes = list(iter_visible_in_frame_range(nodes, + start=start, + end=end)) with suspended_refresh(): with maintained_selection(): diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index f582a106d9..7aa3aaee2a 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, - get_visible_in_frame_range + iter_visible_in_frame_range ) @@ -86,9 +86,9 @@ 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 = get_visible_in_frame_range(nodes, - start=start, - end=end) + nodes = list(iter_visible_in_frame_range(nodes, + start=start, + end=end)) with suspended_refresh(): with maintained_selection(): diff --git a/openpype/hosts/maya/plugins/publish/validate_visible_only.py b/openpype/hosts/maya/plugins/publish/validate_visible_only.py index 9094a421c6..2a10171113 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 get_visible_in_frame_range +from openpype.hosts.maya.api.lib import iter_visible_in_frame_range import openpype.hosts.maya.api.action @@ -40,9 +40,7 @@ class ValidateAlembicVisibleOnly(pyblish.api.InstancePlugin): nodes = instance[:] start, end = cls.get_frame_range(instance) - visible = get_visible_in_frame_range(nodes, start, end) - - if not visible: + if not any(iter_visible_in_frame_range(nodes, start, end)): # Return the nodes we have considered so the user can identify # them with the select invalid action return nodes From a99cc8f4971c52b55104eade2cac6c679db76d0f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 28 Jun 2022 21:30:22 +0200 Subject: [PATCH 60/84] Fix iterator --- openpype/hosts/maya/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 637f9e951b..43f738bfc7 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3272,11 +3272,12 @@ def iter_visible_in_frame_range(nodes, start, end): cmds.currentTime(start) visible = cmds.ls(nodes, long=True, visible=True) + for node in visible: + yield node if len(visible) == len(nodes) or start == end: # All are visible on frame one, so they are at least visible once # inside the frame range. - for node in visible: - yield node + return # For the invisible ones check whether its visibility and/or # any of its parents visibility attributes are animated. If so, it might From af2c57674eb9263587ec082db70e12f69ccf2555 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 29 Jun 2022 15:21:21 +0200 Subject: [PATCH 61/84] 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 62/84] 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 63/84] 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 64/84] 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 65/84] 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 66/84] 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 67/84] 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 aa741a1148c31a8fc76c6fca90ee885b88e7db33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 30 Jun 2022 11:34:34 +0200 Subject: [PATCH 68/84] 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 69/84] 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 70/84] 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 71/84] 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 72/84] :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 752615d0d46992b92270790901cff2acadea701c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 30 Jun 2022 15:14:19 +0200 Subject: [PATCH 73/84] 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 74/84] 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 75/84] 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 76/84] 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 77/84] 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 78/84] 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 79/84] 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 80/84] 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 81/84] 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 82/84] [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 83/84] 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 84/84] 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", {})