From 0fccef46b82247d77ee46019ace506fcb8d3779c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 12:27:25 +0200 Subject: [PATCH 01/40] tags.json moved from presets to nukestudio host --- pype/hosts/nukestudio/tags.json | 262 ++++++++++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 pype/hosts/nukestudio/tags.json diff --git a/pype/hosts/nukestudio/tags.json b/pype/hosts/nukestudio/tags.json new file mode 100644 index 0000000000..56fcfcbce9 --- /dev/null +++ b/pype/hosts/nukestudio/tags.json @@ -0,0 +1,262 @@ +{ + "Hierarchy": { + "editable": "1", + "note": "{folder}/{sequence}/{shot}", + "icon": { + "path": "hierarchy.png" + }, + "metadata": { + "folder": "FOLDER_NAME", + "shot": "{clip}", + "track": "{track}", + "sequence": "{sequence}", + "episode": "EPISODE_NAME", + "root": "{projectroot}" + } + }, + "Source Resolution": { + "editable": "1", + "note": "Use source resolution", + "icon": { + "path": "resolution.png" + }, + "metadata": { + "family": "resolution" + } + }, + "Retiming": { + "editable": "1", + "note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", + "icon": { + "path": "retiming.png" + }, + "metadata": { + "family": "retiming", + "marginIn": 1, + "marginOut": 1 + } + }, + "Frame start": { + "editable": "1", + "note": "Starting frame for comps. \n\n> Use `value` and add either number or write `source` (if you want to preserve source frame numbering)", + "icon": { + "path": "icons:TagBackground.png" + }, + "metadata": { + "family": "frameStart", + "value": "1001" + } + }, + "[Lenses]": { + "Set lense here": { + "editable": "1", + "note": "Adjust parameters of your lense and then drop to clip. Remember! You can always overwrite on clip", + "icon": { + "path": "lense.png" + }, + "metadata": { + "focalLengthMm": 57 + + } + } + }, + "[Subsets]": { + "Audio": { + "editable": "1", + "note": "Export with Audio", + "icon": { + "path": "volume.png" + }, + "metadata": { + "family": "audio", + "subset": "main" + } + }, + "plateFg": { + "editable": "1", + "note": "Add to publish to \"forground\" subset. Change metadata subset name if different order number", + "icon": { + "path": "z_layer_fg.png" + }, + "metadata": { + "family": "plate", + "subset": "Fg01" + } + }, + "plateBg": { + "editable": "1", + "note": "Add to publish to \"background\" subset. Change metadata subset name if different order number", + "icon": { + "path": "z_layer_bg.png" + }, + "metadata": { + "family": "plate", + "subset": "Bg01" + } + }, + "plateRef": { + "editable": "1", + "note": "Add to publish to \"reference\" subset.", + "icon": { + "path": "icons:Reference.png" + }, + "metadata": { + "family": "plate", + "subset": "Ref" + } + }, + "plateMain": { + "editable": "1", + "note": "Add to publish to \"main\" subset.", + "icon": { + "path": "z_layer_main.png" + }, + "metadata": { + "family": "plate", + "subset": "main" + } + }, + "plateProxy": { + "editable": "1", + "note": "Add to publish to \"proxy\" subset.", + "icon": { + "path": "z_layer_main.png" + }, + "metadata": { + "family": "plate", + "subset": "proxy" + } + }, + "review": { + "editable": "1", + "note": "Upload to Ftrack as review component.", + "icon": { + "path": "review.png" + }, + "metadata": { + "family": "review", + "track": "review" + } + } + }, + "[Handles]": { + "start: add 20 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "20", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 10 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "10", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 5 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "5", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 0 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "0", + "args": "{'op':'add','where':'start'}" + } + }, + "end: add 20 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "20", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 10 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "10", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 5 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "5", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 0 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "0", + "args": "{'op':'add','where':'end'}" + } + } + }, + "NukeScript": { + "editable": "1", + "note": "Collecting track items to Nuke scripts.", + "icon": { + "path": "icons:TagNuke.png" + }, + "metadata": { + "family": "nukescript", + "subset": "main" + } + }, + "Comment": { + "editable": "1", + "note": "Comment on a shot.", + "icon": { + "path": "icons:TagComment.png" + }, + "metadata": { + "family": "comment", + "subset": "main" + } + } +} From aa6a7bc79429d42cb8044d34553feb773142f8ca Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Jul 2020 12:31:01 +0200 Subject: [PATCH 02/40] nukestudio tags.py load tags data from json next to it instead of using presets --- pype/hosts/nukestudio/tags.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pype/hosts/nukestudio/tags.py b/pype/hosts/nukestudio/tags.py index c97f13d17c..c2b1d0d728 100644 --- a/pype/hosts/nukestudio/tags.py +++ b/pype/hosts/nukestudio/tags.py @@ -1,16 +1,22 @@ import re import os +import json import hiero -from pype.api import ( - config, - Logger -) +from pype.api import Logger from avalon import io log = Logger().get_logger(__name__, "nukestudio") +def tag_data(): + current_dir = os.path.dirname(__file__) + json_path = os.path.join(current_dir, "tags.json") + with open(json_path, "r") as json_stream: + data = json.load(json_stream) + return data + + def create_tag(key, value): """ Creating Tag object. @@ -58,13 +64,9 @@ def add_tags_from_presets(): return log.debug("Setting default tags on project: {}".format(project.name())) - - # get all presets - presets = config.get_presets() - # get nukestudio tag.json from presets - nks_pres = presets["nukestudio"] - nks_pres_tags = nks_pres.get("tags", None) + # get nukestudio tags.json + nks_pres_tags = tag_data() # Get project task types. tasks = io.find_one({"type": "project"})["config"]["tasks"] From 360264117ee62d92cd58da90fcc9f61567ec0885 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Jul 2020 17:07:07 +0100 Subject: [PATCH 03/40] Adding "audioMain" to Nuke reviews. - also added get_latest_version method to library. --- pype/api.py | 6 ++-- pype/lib.py | 37 +++++++++++++++++++++ pype/plugins/nuke/publish/collect_review.py | 18 ++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/pype/api.py b/pype/api.py index 44a31f2626..ce13688d13 100644 --- a/pype/api.py +++ b/pype/api.py @@ -40,7 +40,8 @@ from .lib import ( get_version_from_path, get_last_version_from_path, modified_environ, - add_tool_to_environment + add_tool_to_environment, + get_latest_version ) # Special naming case for subprocess since its a built-in method. @@ -85,5 +86,6 @@ __all__ = [ "modified_environ", "add_tool_to_environment", - "subprocess" + "subprocess", + "get_latest_version" ] diff --git a/pype/lib.py b/pype/lib.py index 87808e53f5..29e02ffd57 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1387,3 +1387,40 @@ def ffprobe_streams(path_to_file): popen_output = popen.communicate()[0] log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] + + +def get_latest_version(asset_name, subset_name): + """Retrieve latest version from `asset_name`, and `subset_name`. + + Args: + asset_name (str): Name of asset. + subset_name (str): Name of subset. + """ + # Get asset + asset_name = io.find_one( + {"type": "asset", "name": asset_name}, projection={"name": True} + ) + + subset = io.find_one( + {"type": "subset", "name": subset_name, "parent": asset_name["_id"]}, + projection={"_id": True, "name": True}, + ) + + # Check if subsets actually exists. + assert subset, "No subsets found." + + # Get version + version_projection = { + "name": True, + "parent": True, + } + + version = io.find_one( + {"type": "version", "parent": subset["_id"]}, + projection=version_projection, + sort=[("name", -1)], + ) + + assert version, "No version found, this is a bug" + + return version diff --git a/pype/plugins/nuke/publish/collect_review.py b/pype/plugins/nuke/publish/collect_review.py index c95c94541d..e7e8da19a1 100644 --- a/pype/plugins/nuke/publish/collect_review.py +++ b/pype/plugins/nuke/publish/collect_review.py @@ -1,4 +1,7 @@ import pyblish.api +import pype.api +from avalon import io, api + import nuke @@ -23,6 +26,21 @@ class CollectReview(pyblish.api.InstancePlugin): if not node["review"].value(): return + # Add audio to instance if it exists. + try: + version = pype.api.get_latest_version( + instance.context.data["assetEntity"]["name"], "audioMain" + ) + representation = io.find_one( + {"type": "representation", "parent": version["_id"]} + ) + instance.data["audio"] = [{ + "offset": 0, + "filename": api.get_representation_path(representation) + }] + except AssertionError: + pass + instance.data["families"].append("review") instance.data['families'].append('ftrack') From c131ffefbdecf92c446ab1e00ced6051231ccfcb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Jul 2020 11:12:06 +0100 Subject: [PATCH 04/40] Calculate size - option to only calculate without deletion --- .../actions/action_delete_old_versions.py | 105 ++++++++++++++++-- 1 file changed, 93 insertions(+), 12 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index 46652b136a..6a4c5a0cae 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -105,11 +105,34 @@ class DeleteOldVersions(BaseAction): "value": False }) + items.append(self.splitter_item) + + items.append({ + "type": "label", + "value": ( + "This will NOT delete any files and only return the " + "total size of the files." + ) + }) + items.append({ + "type": "boolean", + "name": "only_calculate", + "label": "Only calculate size of files.", + "value": False + }) + return { "items": items, "title": self.inteface_title } + def sizeof_fmt(self, num, suffix='B'): + for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: + if abs(num) < 1024.0: + return "%3.1f%s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f%s%s" % (num, 'Yi', suffix) + def launch(self, session, entities, event): values = event["data"].get("values") if not values: @@ -117,6 +140,7 @@ class DeleteOldVersions(BaseAction): versions_count = int(values["last_versions_count"]) force_to_remove = values["force_delete_publish_folder"] + only_calculate = values["only_calculate"] _val1 = "OFF" if force_to_remove: @@ -318,10 +342,29 @@ class DeleteOldVersions(BaseAction): "Folder does not exist. Deleting it's files skipped: {}" ).format(paths_msg)) + # Size of files. + size = 0 + + if only_calculate: + if force_to_remove: + size = self.delete_whole_dir_paths( + dir_paths.values(), delete=False + ) + else: + size = self.delete_only_repre_files( + dir_paths, file_paths_by_dir, delete=False + ) + + msg = "Total size of files: " + self.sizeof_fmt(size) + + self.log.warning(msg) + + return {"success": True, "message": msg} + if force_to_remove: - self.delete_whole_dir_paths(dir_paths.values()) + size = self.delete_whole_dir_paths(dir_paths.values()) else: - self.delete_only_repre_files(dir_paths, file_paths_by_dir) + size = self.delete_only_repre_files(dir_paths, file_paths_by_dir) mongo_changes_bulk = [] for version in versions: @@ -383,17 +426,31 @@ class DeleteOldVersions(BaseAction): "message": msg } - return True + msg = "Total size of files deleted: " + self.sizeof_fmt(size) + + self.log.warning(msg) + + return {"success": True, "message": msg} + + def delete_whole_dir_paths(self, dir_paths, delete=True): + size = 0 - def delete_whole_dir_paths(self, dir_paths): for dir_path in dir_paths: # Delete all files and fodlers in dir path for root, dirs, files in os.walk(dir_path, topdown=False): for name in files: - os.remove(os.path.join(root, name)) + file_path = os.path.join(root, name) + size += os.path.getsize(file_path) + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) for name in dirs: - os.rmdir(os.path.join(root, name)) + if delete: + os.rmdir(os.path.join(root, name)) + + if not delete: + continue # Delete even the folder and it's parents folders if they are empty while True: @@ -406,7 +463,11 @@ class DeleteOldVersions(BaseAction): os.rmdir(os.path.join(dir_path)) - def delete_only_repre_files(self, dir_paths, file_paths): + return size + + def delete_only_repre_files(self, dir_paths, file_paths, delete=True): + size = 0 + for dir_id, dir_path in dir_paths.items(): dir_files = os.listdir(dir_path) collections, remainders = clique.assemble(dir_files) @@ -420,8 +481,13 @@ class DeleteOldVersions(BaseAction): "File was not found: {}".format(file_path) ) continue - os.remove(file_path) - self.log.debug("Removed file: {}".format(file_path)) + + size += os.path.getsize(file_path) + + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) + remainders.remove(file_path_base) continue @@ -440,21 +506,34 @@ class DeleteOldVersions(BaseAction): final_col.head = os.path.join(dir_path, final_col.head) for _file_path in final_col: if os.path.exists(_file_path): - os.remove(_file_path) + + size += os.path.getsize(_file_path) + + if delete: + os.remove(_file_path) + self.log.debug( + "Removed file: {}".format(_file_path) + ) + _seq_path = final_col.format("{head}{padding}{tail}") self.log.debug("Removed files: {}".format(_seq_path)) collections.remove(final_col) elif os.path.exists(file_path): - os.remove(file_path) - self.log.debug("Removed file: {}".format(file_path)) + size += os.path.getsize(file_path) + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) else: self.log.warning( "File was not found: {}".format(file_path) ) # Delete as much as possible parent folders + if not delete: + return size + for dir_path in dir_paths.values(): while True: if not os.path.exists(dir_path): @@ -467,6 +546,8 @@ class DeleteOldVersions(BaseAction): self.log.debug("Removed folder: {}".format(dir_path)) os.rmdir(dir_path) + return size + def path_from_represenation(self, representation, anatomy): try: template = representation["data"]["template"] From bd7876fe20acd526c56cdca408dd89805e7ce588 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Jul 2020 19:52:20 +0200 Subject: [PATCH 05/40] bake custom attributes to camera during export --- .../maya/publish/extract_camera_alembic.py | 9 ++++ .../maya/publish/extract_camera_mayaAscii.py | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/pype/plugins/maya/publish/extract_camera_alembic.py b/pype/plugins/maya/publish/extract_camera_alembic.py index cc090760ff..c61ec5e19e 100644 --- a/pype/plugins/maya/publish/extract_camera_alembic.py +++ b/pype/plugins/maya/publish/extract_camera_alembic.py @@ -19,6 +19,7 @@ class ExtractCameraAlembic(pype.api.Extractor): label = "Camera (Alembic)" hosts = ["maya"] families = ["camera"] + bake_attributes = [] def process(self, instance): @@ -66,6 +67,14 @@ class ExtractCameraAlembic(pype.api.Extractor): job_str += ' -file "{0}"'.format(path) + # bake specified attributes in preset + assert isinstance(self.bake_attributes, (list, tuple)), ( + "Attributes to bake must be specified as a list" + ) + for attr in self.bake_attributes: + self.log.info("Adding {} attribute".format(attr)) + job_str += " -attr {0}".format(attr) + with lib.evaluation("off"): with avalon.maya.suspended_refresh(): cmds.AbcExport(j=job_str, verbose=False) diff --git a/pype/plugins/maya/publish/extract_camera_mayaAscii.py b/pype/plugins/maya/publish/extract_camera_mayaAscii.py index 973d8d452a..eb3b1671de 100644 --- a/pype/plugins/maya/publish/extract_camera_mayaAscii.py +++ b/pype/plugins/maya/publish/extract_camera_mayaAscii.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract camera to Maya file.""" import os from maya import cmds @@ -65,6 +67,45 @@ def unlock(plug): cmds.disconnectAttr(source, destination) +def bake_attribute(camera, + attributes, + step=1.0, simulation=True, frame_range=None): + """Bake specified attributes on camera. + + Args: + camera (str): Camera name. + attributes (list): List of attributes to bake. + step (float): Animation step used for baking. + simulation (bool): Perform simulation instead of just evaluating + each attribute separately over the range of time. + frame_rage (list, tuple): start and end frame to define range. + + .. See also: + http://download.autodesk.com/us/maya/2011help/Commandspython/bakeResults.html + + """ + + if frame_range is None: + frame_range = [cmds.playbackOptions(query=True, minTime=True), + cmds.playbackOptions(query=True, maxTime=True)] + + # If frame range is single frame bake one frame more, + # otherwise maya.cmds.bakeResults gets confused + if frame_range[1] == frame_range[0]: + frame_range[1] += 1 + + assert isinstance(attributes, (list, tuple)), ( + "Attributes to bake must be specified as a list" + ) + + with lib.keytangent_default(in_tangent_type='auto', + out_tangent_type='auto'): + cmds.bakeResults(camera, attribute=attributes, + simulation=simulation, + time=(frame_range[0], frame_range[1]), + sampleBy=step) + + class ExtractCameraMayaAscii(pype.api.Extractor): """Extract a Camera as Maya Ascii. @@ -84,6 +125,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): label = "Camera (Maya Ascii)" hosts = ["maya"] families = ["camera"] + bake_attributes = [] def process(self, instance): @@ -148,6 +190,14 @@ class ExtractCameraMayaAscii(pype.api.Extractor): unlock(plug) cmds.setAttr(plug, value) + if self.bake_attributes: + self.log.info( + "Baking attributes: {}".format( + self.bake_attributes)) + bake_attribute( + cam, self.bake_attributes, + frame_range=range_with_handles, step=step) + self.log.info("Performing extraction..") cmds.select(baked_shapes, noExpand=True) cmds.file(path, From 0c56ed576d2b12c41896dcc0ba436d1d549f30d3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 01:21:20 +0200 Subject: [PATCH 06/40] logging gui has working filters and sorting with better widgets size on open --- pype/modules/logging/gui/app.py | 10 +- pype/modules/logging/gui/lib.py | 94 --------------- pype/modules/logging/gui/models.py | 179 +++++++++------------------- pype/modules/logging/gui/widgets.py | 79 +++++++----- 4 files changed, 106 insertions(+), 256 deletions(-) delete mode 100644 pype/modules/logging/gui/lib.py diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py index 99b0b230a9..7827bdaf2e 100644 --- a/pype/modules/logging/gui/app.py +++ b/pype/modules/logging/gui/app.py @@ -8,9 +8,9 @@ class LogsWindow(QtWidgets.QWidget): super(LogsWindow, self).__init__(parent) self.setStyleSheet(style.load_stylesheet()) - self.resize(1200, 800) - logs_widget = LogsWidget(parent=self) + self.resize(1400, 800) log_detail = OutputWidget(parent=self) + logs_widget = LogsWidget(log_detail, parent=self) main_layout = QtWidgets.QHBoxLayout() @@ -18,8 +18,6 @@ class LogsWindow(QtWidgets.QWidget): log_splitter.setOrientation(QtCore.Qt.Horizontal) log_splitter.addWidget(logs_widget) log_splitter.addWidget(log_detail) - log_splitter.setStretchFactor(0, 65) - log_splitter.setStretchFactor(1, 35) main_layout.addWidget(log_splitter) @@ -33,5 +31,5 @@ class LogsWindow(QtWidgets.QWidget): def on_selection_changed(self): index = self.logs_widget.selected_log() - node = index.data(self.logs_widget.model.NodeRole) - self.log_detail.set_detail(node) + logs = index.data(self.logs_widget.model.ROLE_LOGS) + self.log_detail.set_detail(logs) diff --git a/pype/modules/logging/gui/lib.py b/pype/modules/logging/gui/lib.py deleted file mode 100644 index 85782e071e..0000000000 --- a/pype/modules/logging/gui/lib.py +++ /dev/null @@ -1,94 +0,0 @@ -import contextlib -from Qt import QtCore - - -def _iter_model_rows( - model, column, include_root=False -): - """Iterate over all row indices in a model""" - indices = [QtCore.QModelIndex()] # start iteration at root - - for index in indices: - # Add children to the iterations - child_rows = model.rowCount(index) - for child_row in range(child_rows): - child_index = model.index(child_row, column, index) - indices.append(child_index) - - if not include_root and not index.isValid(): - continue - - yield index - - -@contextlib.contextmanager -def preserve_states( - tree_view, column=0, role=None, - preserve_expanded=True, preserve_selection=True, - expanded_role=QtCore.Qt.DisplayRole, selection_role=QtCore.Qt.DisplayRole - -): - """Preserves row selection in QTreeView by column's data role. - - This function is created to maintain the selection status of - the model items. When refresh is triggered the items which are expanded - will stay expanded and vise versa. - - tree_view (QWidgets.QTreeView): the tree view nested in the application - column (int): the column to retrieve the data from - role (int): the role which dictates what will be returned - - Returns: - None - - """ - # When `role` is set then override both expanded and selection roles - if role: - expanded_role = role - selection_role = role - - model = tree_view.model() - selection_model = tree_view.selectionModel() - flags = selection_model.Select | selection_model.Rows - - expanded = set() - - if preserve_expanded: - for index in _iter_model_rows( - model, column=column, include_root=False - ): - if tree_view.isExpanded(index): - value = index.data(expanded_role) - expanded.add(value) - - selected = None - - if preserve_selection: - selected_rows = selection_model.selectedRows() - if selected_rows: - selected = set(row.data(selection_role) for row in selected_rows) - - try: - yield - finally: - if expanded: - for index in _iter_model_rows( - model, column=0, include_root=False - ): - value = index.data(expanded_role) - is_expanded = value in expanded - # skip if new index was created meanwhile - if is_expanded is None: - continue - tree_view.setExpanded(index, is_expanded) - - if selected: - # Go through all indices, select the ones with similar data - for index in _iter_model_rows( - model, column=column, include_root=False - ): - value = index.data(selection_role) - state = value in selected - if state: - tree_view.scrollTo(index) # Ensure item is visible - selection_model.select(index, flags) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index ce1fa236a9..b739739b6f 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,21 +1,20 @@ import collections -from Qt import QtCore +from Qt import QtCore, QtGui from pype.api import Logger from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME log = Logger().get_logger("LogModel", "LoggingModule") -class LogModel(QtCore.QAbstractItemModel): - COLUMNS = [ +class LogModel(QtGui.QStandardItemModel): + COLUMNS = ( "process_name", "hostname", "hostip", "username", "system_name", "started" - ] - + ) colums_mapping = { "process_name": "Process Name", "process_id": "Process Id", @@ -25,30 +24,52 @@ class LogModel(QtCore.QAbstractItemModel): "system_name": "System name", "started": "Started at" } - process_keys = [ + process_keys = ( "process_id", "hostname", "hostip", "username", "system_name", "process_name" - ] - log_keys = [ + ) + log_keys = ( "timestamp", "level", "thread", "threadName", "message", "loggerName", "fileName", "module", "method", "lineNumber" - ] + ) default_value = "- Not set -" - NodeRole = QtCore.Qt.UserRole + 1 + + ROLE_LOGS = QtCore.Qt.UserRole + 2 def __init__(self, parent=None): super(LogModel, self).__init__(parent) - self._root_node = Node() + self.log_by_process = None self.dbcon = None + # Crash if connection is not possible to skip this module database = _bootstrap_mongo_log() if LOG_COLLECTION_NAME in database.list_collection_names(): self.dbcon = database[LOG_COLLECTION_NAME] - def add_log(self, log): - node = Node(log) - self._root_node.add_child(node) + def headerData(self, section, orientation, role): + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + ): + if section < len(self.COLUMNS): + key = self.COLUMNS[section] + return self.colums_mapping.get(key, key) + + super(LogModel, self).headerData(section, orientation, role) + + def add_process_logs(self, process_logs): + items = [] + first_item = True + for key in self.COLUMNS: + display_value = str(process_logs[key]) + item = QtGui.QStandardItem(display_value) + if first_item: + first_item = False + item.setData(process_logs["_logs"], self.ROLE_LOGS) + + items.append(item) + self.appendRow(items) def refresh(self): self.log_by_process = collections.defaultdict(list) @@ -65,16 +86,13 @@ class LogModel(QtCore.QAbstractItemModel): continue if process_id not in self.process_info: - proc_dict = {} + proc_dict = {"_logs": []} for key in self.process_keys: proc_dict[key] = ( item.get(key) or self.default_value ) self.process_info[process_id] = proc_dict - if "_logs" not in self.process_info[process_id]: - self.process_info[process_id]["_logs"] = [] - log_item = {} for key in self.log_keys: log_item[key] = item.get(key) or self.default_value @@ -89,114 +107,29 @@ class LogModel(QtCore.QAbstractItemModel): item["_logs"], key=lambda item: item["timestamp"] ) item["started"] = item["_logs"][0]["timestamp"] - self.add_log(item) + self.add_process_logs(item) self.endResetModel() - def data(self, index, role): - if not index.isValid(): - return None - if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: - node = index.internalPointer() - column = index.column() +class LogsFilterProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(LogsFilterProxy, self).__init__(*args, **kwargs) + self.col_usernames = None + self.filter_usernames = set() - key = self.COLUMNS[column] - if key == "started": - return str(node.get(key, None)) - return node.get(key, None) + def update_users_filter(self, users): + self.filter_usernames = set() + for user in users or tuple(): + self.filter_usernames.add(user) + self.invalidateFilter() - if role == self.NodeRole: - return index.internalPointer() - - def index(self, row, column, parent): - """Return index for row/column under parent""" - - if not parent.isValid(): - parent_node = self._root_node - else: - parent_node = parent.internalPointer() - - child_item = parent_node.child(row) - if child_item: - return self.createIndex(row, column, child_item) - return QtCore.QModelIndex() - - def rowCount(self, parent): - node = self._root_node - if parent.isValid(): - node = parent.internalPointer() - return node.childCount() - - def columnCount(self, parent): - return len(self.COLUMNS) - - def parent(self, index): - return QtCore.QModelIndex() - - def headerData(self, section, orientation, role): - if role == QtCore.Qt.DisplayRole: - if section < len(self.COLUMNS): - key = self.COLUMNS[section] - return self.colums_mapping.get(key, key) - - super(LogModel, self).headerData(section, orientation, role) - - def flags(self, index): - return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) - - def clear(self): - self.beginResetModel() - self._root_node = Node() - self.endResetModel() - - -class Node(dict): - """A node that can be represented in a tree view. - - The node can store data just like a dictionary. - - >>> data = {"name": "John", "score": 10} - >>> node = Node(data) - >>> assert node["name"] == "John" - - """ - - def __init__(self, data=None): - super(Node, self).__init__() - - self._children = list() - self._parent = None - - if data is not None: - assert isinstance(data, dict) - self.update(data) - - def childCount(self): - return len(self._children) - - def child(self, row): - if row >= len(self._children): - log.warning("Invalid row as child: {0}".format(row)) - return - - return self._children[row] - - def children(self): - return self._children - - def parent(self): - return self._parent - - def row(self): - """ - Returns: - int: Index of this node under parent""" - if self._parent is not None: - siblings = self.parent().children() - return siblings.index(self) - - def add_child(self, child): - """Add a child to this node""" - child._parent = self - self._children.append(child) + def filterAcceptsRow(self, source_row, source_parent): + if self.col_usernames is not None: + index = self.sourceModel().index( + source_row, self.col_usernames, source_parent + ) + user = index.data(QtCore.Qt.DisplayRole) + if user not in self.filter_usernames: + return False + return True diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index cf20066397..f567cae674 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -1,6 +1,6 @@ from Qt import QtCore, QtWidgets, QtGui from PyQt5.QtCore import QVariant -from .models import LogModel +from .models import LogModel, LogsFilterProxy class SearchComboBox(QtWidgets.QComboBox): @@ -193,54 +193,37 @@ class LogsWidget(QtWidgets.QWidget): active_changed = QtCore.Signal() - def __init__(self, parent=None): + def __init__(self, detail_widget, parent=None): super(LogsWidget, self).__init__(parent=parent) model = LogModel() + proxy_model = LogsFilterProxy() + proxy_model.setSourceModel(model) + proxy_model.col_usernames = model.COLUMNS.index("username") filter_layout = QtWidgets.QHBoxLayout() # user_filter = SearchComboBox(self, "Users") user_filter = CustomCombo("Users", self) - users = model.dbcon.distinct("user") + users = model.dbcon.distinct("username") user_filter.populate(users) user_filter.selection_changed.connect(self.user_changed) + proxy_model.update_users_filter(users) + level_filter = CustomCombo("Levels", self) # levels = [(level, True) for level in model.dbcon.distinct("level")] levels = model.dbcon.distinct("level") level_filter.addItems(levels) + level_filter.selection_changed.connect(self.level_changed) - date_from_label = QtWidgets.QLabel("From:") - date_filter_from = QtWidgets.QDateTimeEdit() - - date_from_layout = QtWidgets.QVBoxLayout() - date_from_layout.addWidget(date_from_label) - date_from_layout.addWidget(date_filter_from) - - # now = datetime.datetime.now() - # QtCore.QDateTime( - # now.year, - # now.month, - # now.day, - # now.hour, - # now.minute, - # second=0, - # msec=0, - # timeSpec=0 - # ) - date_to_label = QtWidgets.QLabel("To:") - date_filter_to = QtWidgets.QDateTimeEdit() - - date_to_layout = QtWidgets.QVBoxLayout() - date_to_layout.addWidget(date_to_label) - date_to_layout.addWidget(date_filter_to) + detail_widget.update_level_filter(levels) filter_layout.addWidget(user_filter) filter_layout.addWidget(level_filter) - filter_layout.addLayout(date_from_layout) - filter_layout.addLayout(date_to_layout) + spacer = QtWidgets.QWidget() + filter_layout.addWidget(spacer, 1) view = QtWidgets.QTreeView(self) view.setAllColumnsShowFocus(True) @@ -250,6 +233,8 @@ class LogsWidget(QtWidgets.QWidget): layout.addLayout(filter_layout) layout.addWidget(view) + view.setModel(proxy_model) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) view.setSortingEnabled(True) view.sortByColumn( @@ -257,24 +242,36 @@ class LogsWidget(QtWidgets.QWidget): QtCore.Qt.AscendingOrder ) - view.setModel(model) view.pressed.connect(self._on_activated) # prepare model.refresh() # Store to memory self.model = model + self.proxy_model = proxy_model self.view = view self.user_filter = user_filter self.level_filter = level_filter + self.detail_widget = detail_widget + def _on_activated(self, *args, **kwargs): self.active_changed.emit() def user_changed(self): + checked_values = set() for action in self.user_filter.items(): - print(action) + if action.isChecked(): + checked_values.add(action.text()) + self.proxy_model.update_users_filter(checked_values) + + def level_changed(self): + checked_values = set() + for action in self.level_filter.items(): + if action.isChecked(): + checked_values.add(action.text()) + self.detail_widget.update_level_filter(checked_values) def on_context_menu(self, point): # TODO will be any actions? it's ready @@ -309,13 +306,29 @@ class OutputWidget(QtWidgets.QWidget): self.setLayout(layout) self.output_text = output_text + self.las_logs = None + self.filter_levels = set() + + def update_level_filter(self, levels): + self.filter_levels = set() + for level in levels or tuple(): + self.filter_levels.add(level.lower()) + + self.set_detail(self.las_logs) + def add_line(self, line): self.output_text.append(line) - def set_detail(self, node): + def set_detail(self, logs): + self.las_logs = logs self.output_text.clear() - for log in node["_logs"]: + if not logs: + return + + for log in logs: level = log["level"].lower() + if level not in self.filter_levels: + continue line_f = "{message}" if level == "debug": From a6423c97120859916b61525ebc63e043e722fe7f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 01:35:28 +0200 Subject: [PATCH 07/40] added timestamp support --- pype/modules/logging/gui/widgets.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index f567cae674..aa044946cb 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -297,18 +297,32 @@ class OutputWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(OutputWidget, self).__init__(parent=parent) layout = QtWidgets.QVBoxLayout(self) + + show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp") + output_text = QtWidgets.QTextEdit() output_text.setReadOnly(True) # output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth) + layout.addWidget(show_timecode_checkbox) layout.addWidget(output_text) + show_timecode_checkbox.stateChanged.connect( + self.on_show_timecode_change + ) self.setLayout(layout) self.output_text = output_text + self.show_timecode_checkbox = show_timecode_checkbox self.las_logs = None self.filter_levels = set() + def show_timecode(self): + return self.show_timecode_checkbox.isChecked() + + def on_show_timecode_change(self): + self.set_detail(self.las_logs) + def update_level_filter(self, levels): self.filter_levels = set() for level in levels or tuple(): @@ -325,6 +339,7 @@ class OutputWidget(QtWidgets.QWidget): if not logs: return + show_timecode = self.show_timecode() for log in logs: level = log["level"].lower() if level not in self.filter_levels: @@ -366,6 +381,10 @@ class OutputWidget(QtWidgets.QWidget): line = line_f.format(**log) + if show_timecode: + timestamp = log["timestamp"] + line = timestamp.strftime("%Y-%d-%m %H:%M:%S") + " " + line + self.add_line(line) if not exc: From 80a26eb9de57c42f0dc1873cc71e2417a1e398c6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 01:59:09 +0200 Subject: [PATCH 08/40] removed unused --- pype/modules/logging/gui/widgets.py | 139 ---------------------------- 1 file changed, 139 deletions(-) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index aa044946cb..51d3095b44 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -50,37 +50,6 @@ class SearchComboBox(QtWidgets.QComboBox): return text -class CheckableComboBox2(QtWidgets.QComboBox): - def __init__(self, parent=None): - super(CheckableComboBox, self).__init__(parent) - self.view().pressed.connect(self.handleItemPressed) - self._changed = False - - def handleItemPressed(self, index): - item = self.model().itemFromIndex(index) - if item.checkState() == QtCore.Qt.Checked: - item.setCheckState(QtCore.Qt.Unchecked) - else: - item.setCheckState(QtCore.Qt.Checked) - self._changed = True - - def hidePopup(self): - if not self._changed: - super(CheckableComboBox, self).hidePopup() - self._changed = False - - def itemChecked(self, index): - item = self.model().item(index, self.modelColumn()) - return item.checkState() == QtCore.Qt.Checked - - def setItemChecked(self, index, checked=True): - item = self.model().item(index, self.modelColumn()) - if checked: - item.setCheckState(QtCore.Qt.Checked) - else: - item.setCheckState(QtCore.Qt.Unchecked) - - class SelectableMenu(QtWidgets.QMenu): selection_changed = QtCore.Signal() @@ -137,57 +106,6 @@ class CustomCombo(QtWidgets.QWidget): yield action -class CheckableComboBox(QtWidgets.QComboBox): - def __init__(self, parent=None): - super(CheckableComboBox, self).__init__(parent) - - view = QtWidgets.QTreeView() - view.header().hide() - view.setRootIsDecorated(False) - - model = QtGui.QStandardItemModel() - - view.pressed.connect(self.handleItemPressed) - self._changed = False - - self.setView(view) - self.setModel(model) - - self.view = view - self.model = model - - def handleItemPressed(self, index): - item = self.model.itemFromIndex(index) - if item.checkState() == QtCore.Qt.Checked: - item.setCheckState(QtCore.Qt.Unchecked) - else: - item.setCheckState(QtCore.Qt.Checked) - self._changed = True - - def hidePopup(self): - if not self._changed: - super(CheckableComboBox, self).hidePopup() - self._changed = False - - def itemChecked(self, index): - item = self.model.item(index, self.modelColumn()) - return item.checkState() == QtCore.Qt.Checked - - def setItemChecked(self, index, checked=True): - item = self.model.item(index, self.modelColumn()) - if checked: - item.setCheckState(QtCore.Qt.Checked) - else: - item.setCheckState(QtCore.Qt.Unchecked) - - def addItems(self, items): - for text, checked in items: - text_item = QtGui.QStandardItem(text) - checked_item = QtGui.QStandardItem() - checked_item.setData(QVariant(checked), QtCore.Qt.CheckStateRole) - self.model.appendRow([text_item, checked_item]) - - class LogsWidget(QtWidgets.QWidget): """A widget that lists the published subsets for an asset""" @@ -391,60 +309,3 @@ class OutputWidget(QtWidgets.QWidget): continue for _line in exc["stackTrace"].split("\n"): self.add_line(_line) - - -class LogDetailWidget(QtWidgets.QWidget): - """A Widget that display information about a specific version""" - data_rows = [ - "user", - "message", - "level", - "logname", - "method", - "module", - "fileName", - "lineNumber", - "host", - "timestamp" - ] - - html_text = u""" -

{user} - {timestamp}

-User
{user}
-
Level
{level}
-
Message
{message}
-
Log Name
{logname}

Method
{method}
-
File
{fileName}
-
Line
{lineNumber}
-
Host
{host}
-
Timestamp
{timestamp}
-""" - - def __init__(self, parent=None): - super(LogDetailWidget, self).__init__(parent=parent) - - layout = QtWidgets.QVBoxLayout(self) - - label = QtWidgets.QLabel("Detail") - detail_widget = QtWidgets.QTextEdit() - detail_widget.setReadOnly(True) - layout.addWidget(label) - layout.addWidget(detail_widget) - - self.detail_widget = detail_widget - - self.setEnabled(True) - - self.set_detail(None) - - def set_detail(self, detail_data): - if not detail_data: - self.detail_widget.setText("") - return - - data = dict() - for row in self.data_rows: - value = detail_data.get(row) or "< Not set >" - data[row] = value - - self.detail_widget.setHtml(self.html_text.format(**data)) From 5c57e183b0efc64e4af1f49bc116e7daf59bf14a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 01:59:35 +0200 Subject: [PATCH 09/40] added refresh --- pype/modules/logging/gui/app.py | 7 ---- pype/modules/logging/gui/models.py | 3 +- pype/modules/logging/gui/widgets.py | 50 +++++++++++++++++++---------- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py index 7827bdaf2e..c0e180c8a1 100644 --- a/pype/modules/logging/gui/app.py +++ b/pype/modules/logging/gui/app.py @@ -26,10 +26,3 @@ class LogsWindow(QtWidgets.QWidget): self.setLayout(main_layout) self.setWindowTitle("Logs") - - self.logs_widget.active_changed.connect(self.on_selection_changed) - - def on_selection_changed(self): - index = self.logs_widget.selected_log() - logs = index.data(self.logs_widget.model.ROLE_LOGS) - self.log_detail.set_detail(logs) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index b739739b6f..ae2666f501 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -35,6 +35,7 @@ class LogModel(QtGui.QStandardItemModel): default_value = "- Not set -" ROLE_LOGS = QtCore.Qt.UserRole + 2 + ROLE_PROCESS_ID = QtCore.Qt.UserRole + 3 def __init__(self, parent=None): super(LogModel, self).__init__(parent) @@ -67,7 +68,7 @@ class LogModel(QtGui.QStandardItemModel): if first_item: first_item = False item.setData(process_logs["_logs"], self.ROLE_LOGS) - + item.setData(process_logs["process_id"], self.ROLE_PROCESS_ID) items.append(item) self.appendRow(items) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 51d3095b44..5304fc4d56 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -1,5 +1,5 @@ -from Qt import QtCore, QtWidgets, QtGui -from PyQt5.QtCore import QVariant +from Qt import QtCore, QtWidgets +from avalon.vendor import qtawesome from .models import LogModel, LogsFilterProxy @@ -109,8 +109,6 @@ class CustomCombo(QtWidgets.QWidget): class LogsWidget(QtWidgets.QWidget): """A widget that lists the published subsets for an asset""" - active_changed = QtCore.Signal() - def __init__(self, detail_widget, parent=None): super(LogsWidget, self).__init__(parent=parent) @@ -125,7 +123,7 @@ class LogsWidget(QtWidgets.QWidget): user_filter = CustomCombo("Users", self) users = model.dbcon.distinct("username") user_filter.populate(users) - user_filter.selection_changed.connect(self.user_changed) + user_filter.selection_changed.connect(self._user_changed) proxy_model.update_users_filter(users) @@ -133,15 +131,19 @@ class LogsWidget(QtWidgets.QWidget): # levels = [(level, True) for level in model.dbcon.distinct("level")] levels = model.dbcon.distinct("level") level_filter.addItems(levels) - level_filter.selection_changed.connect(self.level_changed) + level_filter.selection_changed.connect(self._level_changed) detail_widget.update_level_filter(levels) + spacer = QtWidgets.QWidget() + + icon = qtawesome.icon("fa.refresh", color="white") + refresh_btn = QtWidgets.QPushButton(icon, "") + filter_layout.addWidget(user_filter) filter_layout.addWidget(level_filter) - - spacer = QtWidgets.QWidget() filter_layout.addWidget(spacer, 1) + filter_layout.addWidget(refresh_btn) view = QtWidgets.QTreeView(self) view.setAllColumnsShowFocus(True) @@ -160,9 +162,8 @@ class LogsWidget(QtWidgets.QWidget): QtCore.Qt.AscendingOrder ) - view.pressed.connect(self._on_activated) - # prepare - model.refresh() + view.selectionModel().selectionChanged.connect(self._on_index_change) + refresh_btn.clicked.connect(self._on_refresh_clicked) # Store to memory self.model = model @@ -173,18 +174,33 @@ class LogsWidget(QtWidgets.QWidget): self.level_filter = level_filter self.detail_widget = detail_widget + self.refresh_btn = refresh_btn - def _on_activated(self, *args, **kwargs): - self.active_changed.emit() + # prepare + self.refresh() - def user_changed(self): + def refresh(self): + self.model.refresh() + + def _on_refresh_clicked(self): + self.refresh() + + def _on_index_change(self, to_index, from_index): + index = self._selected_log() + if index: + logs = index.data(self.model.ROLE_LOGS) + else: + logs = [] + self.detail_widget.set_detail(logs) + + def _user_changed(self): checked_values = set() for action in self.user_filter.items(): if action.isChecked(): checked_values.add(action.text()) self.proxy_model.update_users_filter(checked_values) - def level_changed(self): + def _level_changed(self): checked_values = set() for action in self.level_filter.items(): if action.isChecked(): @@ -203,7 +219,7 @@ class LogsWidget(QtWidgets.QWidget): selection = self.view.selectionModel() rows = selection.selectedRows(column=0) - def selected_log(self): + def _selected_log(self): selection = self.view.selectionModel() rows = selection.selectedRows(column=0) if len(rows) == 1: @@ -251,7 +267,7 @@ class OutputWidget(QtWidgets.QWidget): def add_line(self, line): self.output_text.append(line) - def set_detail(self, logs): + def set_detail(self, logs=None): self.las_logs = logs self.output_text.clear() if not logs: From 862a07ac4f9b513ab20b1d613709497dfad5afb6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 02:09:37 +0200 Subject: [PATCH 10/40] refresh enhancement --- pype/modules/logging/gui/widgets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 5304fc4d56..826f32646d 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -181,6 +181,7 @@ class LogsWidget(QtWidgets.QWidget): def refresh(self): self.model.refresh() + self.detail_widget.refresh() def _on_refresh_clicked(self): self.refresh() @@ -248,8 +249,10 @@ class OutputWidget(QtWidgets.QWidget): self.output_text = output_text self.show_timecode_checkbox = show_timecode_checkbox - self.las_logs = None - self.filter_levels = set() + self.refresh() + + def refresh(self): + self.set_detail() def show_timecode(self): return self.show_timecode_checkbox.isChecked() From 5b307448c99aaea8ab41cf3261f8b83a552ea317 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 13:41:44 +0200 Subject: [PATCH 11/40] disable editing --- pype/modules/logging/gui/widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 826f32646d..9b6c0a6a62 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -147,6 +147,7 @@ class LogsWidget(QtWidgets.QWidget): view = QtWidgets.QTreeView(self) view.setAllColumnsShowFocus(True) + view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) From 04e6c06ad1bde4d3375a230f416f1b6e5d888f96 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 26 Jul 2020 13:44:28 +0200 Subject: [PATCH 12/40] reverse sort order --- pype/modules/logging/gui/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 9b6c0a6a62..cd0df283bf 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -160,7 +160,7 @@ class LogsWidget(QtWidgets.QWidget): view.setSortingEnabled(True) view.sortByColumn( model.COLUMNS.index("started"), - QtCore.Qt.AscendingOrder + QtCore.Qt.DescendingOrder ) view.selectionModel().selectionChanged.connect(self._on_index_change) From a47ffde54203adcff6b87cf2d379ab402fea4fb1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Jul 2020 11:05:32 +0200 Subject: [PATCH 13/40] sanitize camera names --- pype/hosts/maya/expected_files.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index a7204cba93..77d55eb1c1 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -158,6 +158,25 @@ class AExpectedFiles: """To be implemented by renderer class.""" pass + def sanitize_camera_name(self, camera): + """Sanitize camera name. + + Remove Maya illegal characters from camera name. + + Args: + camera (str): Maya camera name. + + Returns: + (str): sanitized camera name + + Example: + >>> sanizite_camera_name('test:camera_01') + test_camera_01 + + """ + sanitized = re.sub('[^0-9a-zA-Z_]+', '_', camera) + return sanitized + def get_renderer_prefix(self): """Return prefix for specific renderer. @@ -252,7 +271,7 @@ class AExpectedFiles: mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), + (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), # this is required to remove unfilled aov token, for example # in Redshift (R_REMOVE_AOV_TOKEN, ""), @@ -287,7 +306,8 @@ class AExpectedFiles: mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), + (R_SUBSTITUTE_CAMERA_TOKEN, + self.sanitize_camera_name(cam)), (R_SUBSTITUTE_AOV_TOKEN, aov[0]), (R_CLEAN_FRAME_TOKEN, ""), (R_CLEAN_EXT_TOKEN, ""), @@ -314,7 +334,8 @@ class AExpectedFiles: # camera name to AOV to allow per camera AOVs. aov_name = aov[0] if len(layer_data["cameras"]) > 1: - aov_name = "{}_{}".format(aov[0], cam) + aov_name = "{}_{}".format(aov[0], + self.sanitize_camera_name(cam)) aov_file_list[aov_name] = aov_files file_prefix = layer_data["filePrefix"] From 86119ea823c7436461e6aa4ca3681ea3febcd9d3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Jul 2020 13:37:16 +0200 Subject: [PATCH 14/40] support for updating renderSetup settings --- pype/plugins/maya/load/load_rendersetup.py | 63 +++++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/pype/plugins/maya/load/load_rendersetup.py b/pype/plugins/maya/load/load_rendersetup.py index b38e2988b1..fae79548a5 100644 --- a/pype/plugins/maya/load/load_rendersetup.py +++ b/pype/plugins/maya/load/load_rendersetup.py @@ -1,14 +1,23 @@ -from avalon import api -import maya.app.renderSetup.model.renderSetup as renderSetup -from avalon.maya import lib -from maya import cmds +# -*- coding: utf-8 -*- +"""Load and update RenderSetup settings. + +Working with RenderSetup setting is Maya is done utilizing json files. +When this json is loaded, it will overwrite all settings on RenderSetup +instance. +""" + import json +from avalon import api +from avalon.maya import lib +from pype.hosts.maya import lib as pypelib + +from maya import cmds +import maya.app.renderSetup.model.renderSetup as renderSetup + class RenderSetupLoader(api.Loader): - """ - This will load json preset for RenderSetup, overwriting current one. - """ + """Load json preset for RenderSetup overwriting current one.""" families = ["rendersetup"] representations = ["json"] @@ -19,7 +28,7 @@ class RenderSetupLoader(api.Loader): color = "orange" def load(self, context, name, namespace, data): - + """Load RenderSetup settings.""" from avalon.maya.pipeline import containerise # from pype.hosts.maya.lib import namespaced @@ -48,3 +57,41 @@ class RenderSetupLoader(api.Loader): nodes=nodes, context=context, loader=self.__class__.__name__) + + def remove(self, container): + """Remove RenderSetup settings instance.""" + from maya import cmds + + namespace = container["namespace"] + container_name = container["objectName"] + + self.log.info("Removing '%s' from Maya.." % container["name"]) + + container_content = cmds.sets(container_name, query=True) + nodes = cmds.ls(container_content, long=True) + + nodes.append(container_name) + + try: + cmds.delete(nodes) + except ValueError: + # Already implicitly deleted by Maya upon removing reference + pass + + cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) + + def update(self, container, representation): + """Update RenderSetup setting by overwriting existing settings.""" + pypelib.show_message( + "Render setup update", + "Render setup setting will be overwritten by new version. All " + "setting specified by user not included in loaded version " + "will be lost.") + path = api.get_representation_path(representation) + with open(path, "r") as file: + renderSetup.instance().decode( + json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + + def switch(self, container, representation): + """Switch representations.""" + self.update(container, representation) From 7ce31ba1ecf45a5a03b5c4d3e3d19c1b780c9e4d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 28 Jul 2020 13:14:20 +0200 Subject: [PATCH 15/40] small code fixes --- pype/plugins/maya/load/load_rendersetup.py | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pype/plugins/maya/load/load_rendersetup.py b/pype/plugins/maya/load/load_rendersetup.py index fae79548a5..45a314a9d1 100644 --- a/pype/plugins/maya/load/load_rendersetup.py +++ b/pype/plugins/maya/load/load_rendersetup.py @@ -7,6 +7,8 @@ instance. """ import json +import six +import sys from avalon import api from avalon.maya import lib @@ -38,7 +40,7 @@ class RenderSetupLoader(api.Loader): prefix="_" if asset[0].isdigit() else "", suffix="_", ) - + self.log.info(">>> loading json [ {} ]".format(self.fname)) with open(self.fname, "r") as file: renderSetup.instance().decode( json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) @@ -51,6 +53,7 @@ class RenderSetupLoader(api.Loader): if not nodes: return + self.log.info(">>> containerising [ {} ]".format(name)) return containerise( name=name, namespace=namespace, @@ -62,7 +65,6 @@ class RenderSetupLoader(api.Loader): """Remove RenderSetup settings instance.""" from maya import cmds - namespace = container["namespace"] container_name = container["objectName"] self.log.info("Removing '%s' from Maya.." % container["name"]) @@ -78,8 +80,6 @@ class RenderSetupLoader(api.Loader): # Already implicitly deleted by Maya upon removing reference pass - cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) - def update(self, container, representation): """Update RenderSetup setting by overwriting existing settings.""" pypelib.show_message( @@ -89,8 +89,19 @@ class RenderSetupLoader(api.Loader): "will be lost.") path = api.get_representation_path(representation) with open(path, "r") as file: - renderSetup.instance().decode( - json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + try: + renderSetup.instance().decode( + json.load(file), renderSetup.DECODE_AND_OVERWRITE, None) + except Exception: + self.log.error("There were errors during loading") + six.reraise(*sys.exc_info()) + + # Update metadata + node = container["objectName"] + cmds.setAttr("{}.representation".format(node), + str(representation["_id"]), + type="string") + self.log.info("... updated") def switch(self, container, representation): """Switch representations.""" From 9527904cff10c930f6d8238dc85a71a1b758a856 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Jul 2020 14:26:25 +0200 Subject: [PATCH 16/40] fix(celaction): version up method taken fro pype.lib --- .../celaction/publish/integrate_version_up.py | 52 +------------------ 1 file changed, 2 insertions(+), 50 deletions(-) diff --git a/pype/plugins/celaction/publish/integrate_version_up.py b/pype/plugins/celaction/publish/integrate_version_up.py index 1822ceabcb..e15c5d5bf6 100644 --- a/pype/plugins/celaction/publish/integrate_version_up.py +++ b/pype/plugins/celaction/publish/integrate_version_up.py @@ -1,5 +1,5 @@ import shutil -import re +import pype import pyblish.api @@ -12,57 +12,9 @@ class VersionUpScene(pyblish.api.ContextPlugin): def process(self, context): current_file = context.data.get('currentFile') - v_up = get_version_up(current_file) + v_up = pype.lib.version_up(current_file) self.log.debug('Current file is: {}'.format(current_file)) self.log.debug('Version up: {}'.format(v_up)) shutil.copy2(current_file, v_up) self.log.info('Scene saved into new version: {}'.format(v_up)) - - -def version_get(string, prefix, suffix=None): - """Extract version information from filenames used by DD (and Weta, apparently) - These are _v# or /v# or .v# where v is a prefix string, in our case - we use "v" for render version and "c" for camera track version. - See the version.py and camera.py plugins for usage.""" - - if string is None: - raise ValueError("Empty version string - no match") - - regex = r"[/_.]{}\d+".format(prefix) - matches = re.findall(regex, string, re.IGNORECASE) - if not len(matches): - msg = f"No `_{prefix}#` found in `{string}`" - raise ValueError(msg) - return (matches[-1:][0][1], re.search(r"\d+", matches[-1:][0]).group()) - - -def version_set(string, prefix, oldintval, newintval): - """Changes version information from filenames used by DD (and Weta, apparently) - These are _v# or /v# or .v# where v is a prefix string, in our case - we use "v" for render version and "c" for camera track version. - See the version.py and camera.py plugins for usage.""" - - regex = r"[/_.]{}\d+".format(prefix) - matches = re.findall(regex, string, re.IGNORECASE) - if not len(matches): - return "" - - # Filter to retain only version strings with matching numbers - matches = filter(lambda s: int(s[2:]) == oldintval, matches) - - # Replace all version strings with matching numbers - for match in matches: - # use expression instead of expr so 0 prefix does not make octal - fmt = "%%(#)0%dd" % (len(match) - 2) - newfullvalue = match[0] + prefix + str(fmt % {"#": newintval}) - string = re.sub(match, newfullvalue, string) - return string - - -def get_version_up(path): - """ Returns the next version of the path """ - - (prefix, v) = version_get(path, 'v') - v = int(v) - return version_set(path, prefix, v, v + 1) From 2a4d075ae5095d077068d47534ae2aceee63d794 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 28 Jul 2020 15:30:22 +0200 Subject: [PATCH 17/40] plugins extract review and extract image in photoshop does not use SaveAs to folder without specifying the filename --- pype/plugins/photoshop/publish/extract_image.py | 16 ++++++++-------- pype/plugins/photoshop/publish/extract_review.py | 11 +++++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py index da3197c7da..1bb13bce6b 100644 --- a/pype/plugins/photoshop/publish/extract_image.py +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -36,16 +36,16 @@ class ExtractImage(pype.api.Extractor): "png": photoshop.com_objects.PNGSaveOptions(), "jpg": photoshop.com_objects.JPEGSaveOptions() } - + file_basename = os.path.splitext( + photoshop.app().ActiveDocument.Name + )[0] for extension, save_option in save_options.items(): + _filename = "{}.{}".format(file_basename, extension) + files[extension] = _filename + + full_filename = os.path.join(staging_dir, _filename) photoshop.app().ActiveDocument.SaveAs( - staging_dir, save_option, True - ) - files[extension] = "{} copy.{}".format( - os.path.splitext( - photoshop.app().ActiveDocument.Name - )[0], - extension + full_filename, save_option, True ) representations = [] diff --git a/pype/plugins/photoshop/publish/extract_review.py b/pype/plugins/photoshop/publish/extract_review.py index 8aebd1ea87..078ee53899 100644 --- a/pype/plugins/photoshop/publish/extract_review.py +++ b/pype/plugins/photoshop/publish/extract_review.py @@ -24,9 +24,10 @@ class ExtractReview(pype.api.Extractor): layers.append(image_instance[0]) # Perform extraction - output_image = "{} copy.jpg".format( + output_image = "{}.jpg".format( os.path.splitext(photoshop.app().ActiveDocument.Name)[0] ) + output_image_path = os.path.join(staging_dir, output_image) with photoshop.maintained_visibility(): # Hide all other layers. extract_ids = [ @@ -39,7 +40,9 @@ class ExtractReview(pype.api.Extractor): layer.Visible = False photoshop.app().ActiveDocument.SaveAs( - staging_dir, photoshop.com_objects.JPEGSaveOptions(), True + output_image_path, + photoshop.com_objects.JPEGSaveOptions(), + True ) ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") @@ -56,7 +59,7 @@ class ExtractReview(pype.api.Extractor): thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg") args = [ ffmpeg_path, "-y", - "-i", os.path.join(staging_dir, output_image), + "-i", output_image_path, "-vf", "scale=300:-1", "-vframes", "1", thumbnail_path @@ -77,7 +80,7 @@ class ExtractReview(pype.api.Extractor): mov_path = os.path.join(staging_dir, "review.mov") args = [ ffmpeg_path, "-y", - "-i", os.path.join(staging_dir, output_image), + "-i", output_image_path, "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", "-vframes", "1", mov_path From 2d553e5cc405a2fc42fc3858b09884760a476e3f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Jul 2020 15:39:18 +0200 Subject: [PATCH 18/40] feat(celactoin, global): metadata json folder fixes on path --- .../celaction/publish/collect_render_path.py | 13 ++++++++----- pype/plugins/global/publish/submit_publish_job.py | 10 +++++++++- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py index a3918a52b6..9cbb0e4880 100644 --- a/pype/plugins/celaction/publish/collect_render_path.py +++ b/pype/plugins/celaction/publish/collect_render_path.py @@ -12,7 +12,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin): # Presets anatomy_render_key = None - anatomy_publish_render_key = None + publish_render_metadata = None def process(self, instance): anatomy = instance.context.data["anatomy"] @@ -28,7 +28,7 @@ class CollectRenderPath(pyblish.api.InstancePlugin): # get anatomy rendering keys anatomy_render_key = self.anatomy_render_key or "render" - anatomy_publish_render_key = self.anatomy_publish_render_key or "render" + publish_render_metadata = self.publish_render_metadata or "render" # get folder and path for rendering images from celaction render_dir = anatomy_filled[anatomy_render_key]["folder"] @@ -46,8 +46,11 @@ class CollectRenderPath(pyblish.api.InstancePlugin): instance.data["path"] = render_path # get anatomy for published renders folder path - if anatomy_filled.get(anatomy_publish_render_key): - instance.data["publishRenderFolder"] = anatomy_filled[ - anatomy_publish_render_key]["folder"] + if anatomy_filled.get(publish_render_metadata): + instance.data["publishRenderMetadataFolder"] = anatomy_filled[ + publish_render_metadata]["folder"] + self.log.info("Metadata render path: `{}`".format( + instance.data["publishRenderMetadataFolder"] + )) self.log.info(f"Render output path set to: `{render_path}`") diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 9f89466c31..e7a426097f 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -206,7 +206,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): def _create_metadata_path(self, instance): ins_data = instance.data # Ensure output dir exists - output_dir = ins_data.get("publishRenderFolder", ins_data["outputDir"]) + output_dir = ins_data.get( + "publishRenderMetadataFolder", ins_data["outputDir"]) try: if not os.path.isdir(output_dir): @@ -715,6 +716,13 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if item in instance.data.get("families", []): instance_skeleton_data["families"] += [item] + if "render.farm" in instance.data["families"]: + instance_skeleton_data.update({ + "family": "render2d", + "families": ["render"] + [f for f in instance.data["families"] + if "render.farm" not in f] + }) + # transfer specific properties from original instance based on # mapping dictionary `instance_transfer` for key, values in self.instance_transfer.items(): From 7df271afb3e7dd32516eff24e6fed22bf4d3ef24 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Jul 2020 15:57:56 +0200 Subject: [PATCH 19/40] fix(global): unneeded code --- pype/plugins/global/publish/submit_publish_job.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index e7a426097f..1ea31d4a0b 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -716,13 +716,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if item in instance.data.get("families", []): instance_skeleton_data["families"] += [item] - if "render.farm" in instance.data["families"]: - instance_skeleton_data.update({ - "family": "render2d", - "families": ["render"] + [f for f in instance.data["families"] - if "render.farm" not in f] - }) - # transfer specific properties from original instance based on # mapping dictionary `instance_transfer` for key, values in self.instance_transfer.items(): From edf61b7c28781b710dd3ce1f5495ec21e2002ed6 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 29 Jul 2020 10:51:23 +0200 Subject: [PATCH 20/40] bump version to 2.11.1 --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 7f6646a762..200c236308 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.11.0" +__version__ = "2.11.1" From 37b2188919d5744c2cf515962e0aab6be3cf0c58 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 29 Jul 2020 14:20:40 +0200 Subject: [PATCH 21/40] location_path is prepared for refilling the root key --- pype/modules/ftrack/actions/action_delivery.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index d4b86d1278..7ae7de65b1 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -293,6 +293,20 @@ class Delivery(BaseAction): repres_to_deliver.append(repre) anatomy = Anatomy(project_name) + + format_dict = {} + if location_path: + location_path = location_path.replace("\\", "/") + root_names = anatomy.root_names_from_templates( + anatomy.templates["delivery"] + ) + if root_names is None: + format_dict["root"] = location_path + else: + format_dict["root"] = {} + for name in root_names: + format_dict["root"][name] = location_path + for repre in repres_to_deliver: # Get destination repre path anatomy_data = copy.deepcopy(repre["context"]) From 7b7d4aa7a8fc59fef0d9523e5b3ae763a897056e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 29 Jul 2020 14:21:26 +0200 Subject: [PATCH 22/40] processing methods format path with entered location --- .../modules/ftrack/actions/action_delivery.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 7ae7de65b1..a50603b2eb 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -368,10 +368,15 @@ class Delivery(BaseAction): return self.report() def process_single_file( - self, repre_path, anatomy, anatomy_name, anatomy_data + self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict ): anatomy_filled = anatomy.format(anatomy_data) - delivery_path = anatomy_filled["delivery"][anatomy_name] + if format_dict: + template_result = anatomy_filled["delivery"][anatomy_name] + delivery_path = template_result.rootless.format(**format_dict) + else: + delivery_path = anatomy_filled["delivery"][anatomy_name] + delivery_folder = os.path.dirname(delivery_path) if not os.path.exists(delivery_folder): os.makedirs(delivery_folder) @@ -379,7 +384,7 @@ class Delivery(BaseAction): self.copy_file(repre_path, delivery_path) def process_sequence( - self, repre_path, anatomy, anatomy_name, anatomy_data + self, repre_path, anatomy, anatomy_name, anatomy_data, format_dict ): dir_path, file_name = os.path.split(str(repre_path)) @@ -422,8 +427,12 @@ class Delivery(BaseAction): anatomy_data["frame"] = frame_indicator anatomy_filled = anatomy.format(anatomy_data) - delivery_path = anatomy_filled["delivery"][anatomy_name] - print(delivery_path) + if format_dict: + template_result = anatomy_filled["delivery"][anatomy_name] + delivery_path = template_result.rootless.format(**format_dict) + else: + delivery_path = anatomy_filled["delivery"][anatomy_name] + delivery_folder = os.path.dirname(delivery_path) dst_head, dst_tail = delivery_path.split(frame_indicator) dst_padding = src_collection.padding From bb9e5ef4f90d00a8a86f26ea6d195f7a8c2aaee5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 29 Jul 2020 14:21:57 +0200 Subject: [PATCH 23/40] args for proces methods are pre-pared --- pype/modules/ftrack/actions/action_delivery.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index a50603b2eb..06257f32d5 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -353,15 +353,18 @@ class Delivery(BaseAction): repre_path = self.path_from_represenation(repre, anatomy) # TODO add backup solution where root of path from component # is repalced with root - if not frame: - self.process_single_file( - repre_path, anatomy, anatomy_name, anatomy_data - ) + args = ( + repre_path, + anatomy, + anatomy_name, + anatomy_data, + format_dict + ) + if not frame: + self.process_single_file(*args) else: - self.process_sequence( - repre_path, anatomy, anatomy_name, anatomy_data - ) + self.process_sequence(*args) self.db_con.uninstall() From 832fd9fa66b67c5a6dacb7817745f84cfbea4e4b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Jul 2020 16:17:15 +0100 Subject: [PATCH 24/40] Fix multiple attributes on the same node overwriting. --- pype/plugins/maya/publish/validate_attributes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pype/plugins/maya/publish/validate_attributes.py b/pype/plugins/maya/publish/validate_attributes.py index 6ecebfa107..a77fbe5e93 100644 --- a/pype/plugins/maya/publish/validate_attributes.py +++ b/pype/plugins/maya/publish/validate_attributes.py @@ -62,9 +62,16 @@ class ValidateAttributes(pyblish.api.ContextPlugin): for family in families: for preset in presets[family]: [node_name, attribute_name] = preset.split(".") - attributes.update( - {node_name: {attribute_name: presets[family][preset]}} - ) + try: + attributes[node_name].update( + {attribute_name: presets[family][preset]} + ) + except KeyError: + attributes.update({ + node_name: { + attribute_name: presets[family][preset] + } + }) # Get invalid attributes. nodes = pm.ls() From 3925ddc5aeabe392230224247bd909c73031fed2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 29 Jul 2020 17:34:54 +0100 Subject: [PATCH 25/40] Instance in Same Context for Nuke Moved the equivalent Maya plugin into global and merged with Nuke. Fixed Nuke instances collection not using node data for asset. --- .../publish/validate_instance_in_context.py | 133 ++++++++++++++++++ .../publish/validate_instance_in_context.py | 108 -------------- .../plugins/nuke/publish/collect_instances.py | 2 +- 3 files changed, 134 insertions(+), 109 deletions(-) create mode 100644 pype/plugins/global/publish/validate_instance_in_context.py delete mode 100644 pype/plugins/maya/publish/validate_instance_in_context.py diff --git a/pype/plugins/global/publish/validate_instance_in_context.py b/pype/plugins/global/publish/validate_instance_in_context.py new file mode 100644 index 0000000000..a4fc555161 --- /dev/null +++ b/pype/plugins/global/publish/validate_instance_in_context.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +"""Validate if instance asset is the same as context asset.""" +from __future__ import absolute_import + +import pyblish.api +import pype.api + + +class SelectInvalidInstances(pyblish.api.Action): + """Select invalid instances in Outliner.""" + + label = "Select Instances" + icon = "briefcase" + on = "failed" + + def process(self, context, plugin): + """Process invalid validators and select invalid instances.""" + # Get the errored instances + failed = [] + for result in context.data["results"]: + if result["error"] is None: + continue + if result["instance"] is None: + continue + if result["instance"] in failed: + continue + if result["plugin"] != plugin: + continue + + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + if instances: + self.log.info( + "Selecting invalid nodes: %s" % ", ".join( + [str(x) for x in instances] + ) + ) + self.select(instances) + else: + self.log.info("No invalid nodes found.") + self.deselect() + + def select(self, instances): + if "nuke" in pyblish.api.registered_hosts(): + import avalon.nuke.lib + import nuke + avalon.nuke.lib.select_nodes( + [nuke.toNode(str(x)) for x in instances] + ) + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.select(instances, replace=True, noExpand=True) + + def deselect(self): + if "nuke" in pyblish.api.registered_hosts(): + import avalon.nuke.lib + avalon.nuke.lib.reset_selection() + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.select(deselect=True) + + +class RepairSelectInvalidInstances(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + # Get the errored instances + failed = [] + for result in context.data["results"]: + if result["error"] is None: + continue + if result["instance"] is None: + continue + if result["instance"] in failed: + continue + if result["plugin"] != plugin: + continue + + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + context_asset = context.data["assetEntity"]["name"] + for instance in instances: + self.set_attribute(instance, context_asset) + + def set_attribute(self, instance, context_asset): + if "nuke" in pyblish.api.registered_hosts(): + import nuke + nuke.toNode( + instance.data.get("name") + )["avalon:asset"].setValue(context_asset) + + if "maya" in pyblish.api.registered_hosts(): + from maya import cmds + cmds.setAttr( + instance.data.get("name") + ".asset", + context_asset, + type="string" + ) + + +class ValidateInstanceInContext(pyblish.api.InstancePlugin): + """Validator to check if instance asset match context asset. + + When working in per-shot style you always publish data in context of + current asset (shot). This validator checks if this is so. It is optional + so it can be disabled when needed. + + Action on this validator will select invalid instances in Outliner. + """ + + order = pype.api.ValidateContentsOrder + label = "Instance in same Context" + optional = True + hosts = ["maya", "nuke"] + actions = [SelectInvalidInstances, RepairSelectInvalidInstances] + + def process(self, instance): + asset = instance.data.get("asset") + context_asset = instance.context.data["assetEntity"]["name"] + msg = "{} has asset {}".format(instance.name, asset) + assert asset == context_asset, msg diff --git a/pype/plugins/maya/publish/validate_instance_in_context.py b/pype/plugins/maya/publish/validate_instance_in_context.py deleted file mode 100644 index 542249bb2d..0000000000 --- a/pype/plugins/maya/publish/validate_instance_in_context.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -"""Validate if instance asset is the same as context asset.""" -from __future__ import absolute_import -import pyblish.api -from pype.action import get_errored_instances_from_context -import pype.api - - -class SelectInvalidInstances(pyblish.api.Action): - """Select invalid instances in Outliner.""" - - label = "Show Instances" - icon = "briefcase" - on = "failed" - - def process(self, context, plugin): - """Process invalid validators and select invalid instances.""" - try: - from maya import cmds - except ImportError: - raise ImportError("Current host is not Maya") - - errored_instances = get_errored_instances_from_context(context) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(errored_instances, plugin) - - # Get the invalid nodes for the plug-ins - self.log.info("Finding invalid nodes..") - invalid = list() - for _instance in instances: - invalid_instances = plugin.get_invalid(context) - if invalid_instances: - if isinstance(invalid_instances, (list, tuple)): - invalid.extend(invalid_instances) - else: - self.log.warning("Plug-in returned to be invalid, " - "but has no selectable nodes.") - - # Ensure unique (process each node only once) - invalid = list(set(invalid)) - - if invalid: - self.log.info("Selecting invalid nodes: %s" % ", ".join(invalid)) - cmds.select(invalid, replace=True, noExpand=True) - else: - self.log.info("No invalid nodes found.") - cmds.select(deselect=True) - - -class RepairSelectInvalidInstances(pyblish.api.Action): - """Repair the instance asset.""" - - label = "Repair" - icon = "wrench" - on = "failed" - - def process(self, context, plugin): - from maya import cmds - # Get the errored instances - failed = [] - for result in context.data["results"]: - if (result["error"] is not None and result["instance"] is not None - and result["instance"] not in failed): - failed.append(result["instance"]) - - # Apply pyblish.logic to get the instances for the plug-in - instances = pyblish.api.instances_by_plugin(failed, plugin) - context_asset = context.data["assetEntity"]["name"] - for instance in instances: - cmds.setAttr(instance.data.get("name") + ".asset", - context_asset, type="string") - - -class ValidateInstanceInContext(pyblish.api.ContextPlugin): - """Validator to check if instance asset match context asset. - - When working in per-shot style you always publish data in context of - current asset (shot). This validator checks if this is so. It is optional - so it can be disabled when needed. - - Action on this validator will select invalid instances in Outliner. - """ - - order = pype.api.ValidateContentsOrder - label = "Instance in same Context" - optional = True - actions = [SelectInvalidInstances, RepairSelectInvalidInstances] - - @classmethod - def get_invalid(cls, context): - """Get invalid instances.""" - invalid = [] - context_asset = context.data["assetEntity"]["name"] - cls.log.info("we are in {}".format(context_asset)) - for instance in context: - asset = instance.data.get("asset") - if asset != context_asset: - cls.log.warning("{} has asset {}".format(instance.name, asset)) - invalid.append(instance.name) - - return invalid - - def process(self, context): - """Check instances.""" - invalid = self.get_invalid(context) - if invalid: - raise AssertionError("Some instances doesn't share same context") diff --git a/pype/plugins/nuke/publish/collect_instances.py b/pype/plugins/nuke/publish/collect_instances.py index 0bbede11c0..9085e12bd8 100644 --- a/pype/plugins/nuke/publish/collect_instances.py +++ b/pype/plugins/nuke/publish/collect_instances.py @@ -106,7 +106,7 @@ class CollectNukeInstances(pyblish.api.ContextPlugin): instance.data.update({ "subset": subset, - "asset": os.environ["AVALON_ASSET"], + "asset": avalon_knob_data["asset"], "label": node.name(), "name": node.name(), "subset": subset, From 907ebe17604142a2487879a9004247bc81cf2c54 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 30 Jul 2020 08:46:53 +0100 Subject: [PATCH 26/40] Fix viewer input process node return as Viewer node --- pype/hosts/nuke/lib.py | 2 +- pype/plugins/nuke/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 72a8836a03..8c0e37b15d 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -1445,7 +1445,7 @@ class ExporterReview: anlib.reset_selection() ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" in n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: diff --git a/pype/plugins/nuke/publish/extract_thumbnail.py b/pype/plugins/nuke/publish/extract_thumbnail.py index 5e9302a01a..a3ef09bc9f 100644 --- a/pype/plugins/nuke/publish/extract_thumbnail.py +++ b/pype/plugins/nuke/publish/extract_thumbnail.py @@ -152,7 +152,7 @@ class ExtractThumbnail(pype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" in n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From 8934fba38883963349d5773de9dd3c7535bd35fc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 30 Jul 2020 17:45:05 +0200 Subject: [PATCH 27/40] make png and jpeg configurable in config --- pype/plugins/photoshop/publish/extract_image.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py index 1bb13bce6b..6dfccdc4f2 100644 --- a/pype/plugins/photoshop/publish/extract_image.py +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -13,6 +13,7 @@ class ExtractImage(pype.api.Extractor): label = "Extract Image" hosts = ["photoshop"] families = ["image"] + formats = ["png", "jpg"] def process(self, instance): @@ -32,10 +33,12 @@ class ExtractImage(pype.api.Extractor): if layer.id not in extract_ids: layer.Visible = False - save_options = { - "png": photoshop.com_objects.PNGSaveOptions(), - "jpg": photoshop.com_objects.JPEGSaveOptions() - } + save_options = {} + if "png" in self.formats: + save_options["png"] = photoshop.com_objects.PNGSaveOptions() + if "jpg" in self.formats: + save_options["jpg"] = photoshop.com_objects.JPEGSaveOptions() + file_basename = os.path.splitext( photoshop.app().ActiveDocument.Name )[0] From c59eeab6e60aae9f01bcd399d387259e41646a2d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 31 Jul 2020 15:35:06 +0200 Subject: [PATCH 28/40] some code cleanup --- pype/plugins/global/publish/cleanup.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/pype/plugins/global/publish/cleanup.py b/pype/plugins/global/publish/cleanup.py index 3ab41f90ca..bca540078f 100644 --- a/pype/plugins/global/publish/cleanup.py +++ b/pype/plugins/global/publish/cleanup.py @@ -1,11 +1,18 @@ +# -*- coding: utf-8 -*- +"""Cleanup leftover files from publish.""" import os import shutil import pyblish.api def clean_renders(instance): - transfers = instance.data.get("transfers", list()) + """Delete renders after publishing. + Args: + instance (pyblish.api.Instace): Instance to work on. + + """ + transfers = instance.data.get("transfers", list()) current_families = instance.data.get("families", list()) instance_family = instance.data.get("family", None) dirnames = [] @@ -40,6 +47,7 @@ class CleanUp(pyblish.api.InstancePlugin): active = True def process(self, instance): + """Plugin entry point.""" # Get the errored instances failed = [] for result in instance.context.data["results"]: @@ -52,7 +60,7 @@ class CleanUp(pyblish.api.InstancePlugin): ) ) - self.log.info("Cleaning renders ...") + self.log.info("Performing cleanup on {}".format(instance)) clean_renders(instance) if [ef for ef in self.exclude_families @@ -60,16 +68,17 @@ class CleanUp(pyblish.api.InstancePlugin): return import tempfile - staging_dir = instance.data.get("stagingDir", None) - if not staging_dir or not os.path.exists(staging_dir): - self.log.info("No staging directory found: %s" % staging_dir) - return - temp_root = tempfile.gettempdir() + staging_dir = instance.data.get("stagingDir", None) + if not os.path.normpath(staging_dir).startswith(temp_root): self.log.info("Skipping cleanup. Staging directory is not in the " "temp folder: %s" % staging_dir) return - self.log.info("Removing staging directory ...") + if not staging_dir or not os.path.exists(staging_dir): + self.log.info("No staging directory found: %s" % staging_dir) + return + + self.log.info("Removing staging directory {}".format(staging_dir)) shutil.rmtree(staging_dir) From 5b21943ed840e635634339158e532e69a4b3b7d3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 31 Jul 2020 17:43:57 +0200 Subject: [PATCH 29/40] make decision about ma/mb configurable per family --- ...yaAscii.py => extract_camera_mayaScene.py} | 39 ++++++++++++----- ...ascii_raw.py => extract_maya_scene_raw.py} | 35 +++++++++++----- pype/plugins/maya/publish/extract_model.py | 32 ++++++++++---- pype/plugins/maya/publish/extract_rig.py | 33 +++++++++++---- pype/plugins/maya/publish/extract_yeti_rig.py | 42 +++++++++++++------ 5 files changed, 131 insertions(+), 50 deletions(-) rename pype/plugins/maya/publish/{extract_camera_mayaAscii.py => extract_camera_mayaScene.py} (82%) rename pype/plugins/maya/publish/{extract_maya_ascii_raw.py => extract_maya_scene_raw.py} (60%) diff --git a/pype/plugins/maya/publish/extract_camera_mayaAscii.py b/pype/plugins/maya/publish/extract_camera_mayaScene.py similarity index 82% rename from pype/plugins/maya/publish/extract_camera_mayaAscii.py rename to pype/plugins/maya/publish/extract_camera_mayaScene.py index 973d8d452a..03dde031e9 100644 --- a/pype/plugins/maya/publish/extract_camera_mayaAscii.py +++ b/pype/plugins/maya/publish/extract_camera_mayaScene.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract camera as Maya Scene.""" import os from maya import cmds @@ -65,8 +67,8 @@ def unlock(plug): cmds.disconnectAttr(source, destination) -class ExtractCameraMayaAscii(pype.api.Extractor): - """Extract a Camera as Maya Ascii. +class ExtractCameraMayaScene(pype.api.Extractor): + """Extract a Camera as Maya Scene. This will create a duplicate of the camera that will be baked *with* substeps and handles for the required frames. This temporary duplicate @@ -81,13 +83,28 @@ class ExtractCameraMayaAscii(pype.api.Extractor): """ - label = "Camera (Maya Ascii)" + label = "Camera (Maya Scene)" hosts = ["maya"] families = ["camera"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" # get settings + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass + framerange = [instance.data.get("frameStart", 1), instance.data.get("frameEnd", 1)] handles = instance.data.get("handles", 0) @@ -95,7 +112,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): bake_to_worldspace = instance.data("bakeToWorldSpace", True) if not bake_to_worldspace: - self.log.warning("Camera (Maya Ascii) export only supports world" + self.log.warning("Camera (Maya Scene) export only supports world" "space baked camera extractions. The disabled " "bake to world space is ignored...") @@ -115,7 +132,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): # Define extract output file path dir_path = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) + filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Perform extraction @@ -152,7 +169,7 @@ class ExtractCameraMayaAscii(pype.api.Extractor): cmds.select(baked_shapes, noExpand=True) cmds.file(path, force=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, constructionHistory=False, @@ -164,15 +181,15 @@ class ExtractCameraMayaAscii(pype.api.Extractor): # Delete the baked hierarchy if bake_to_worldspace: cmds.delete(baked) - - massage_ma_file(path) + if self.scene_type == "ma": + massage_ma_file(path) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'ma', - 'ext': 'ma', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path, } diff --git a/pype/plugins/maya/publish/extract_maya_ascii_raw.py b/pype/plugins/maya/publish/extract_maya_scene_raw.py similarity index 60% rename from pype/plugins/maya/publish/extract_maya_ascii_raw.py rename to pype/plugins/maya/publish/extract_maya_scene_raw.py index 895b6acbfe..2971572552 100644 --- a/pype/plugins/maya/publish/extract_maya_ascii_raw.py +++ b/pype/plugins/maya/publish/extract_maya_scene_raw.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract data as Maya scene (raw).""" import os from maya import cmds @@ -6,24 +8,37 @@ import avalon.maya import pype.api -class ExtractMayaAsciiRaw(pype.api.Extractor): - """Extract as Maya Ascii (raw) +class ExtractMayaSceneRaw(pype.api.Extractor): + """Extract as Maya Scene (raw). This will preserve all references, construction history, etc. - """ - label = "Maya ASCII (Raw)" + label = "Maya Scene (Raw)" hosts = ["maya"] families = ["mayaAscii", "setdress", "layout"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass # Define extract output file path dir_path = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) + filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Whether to include all nodes in the instance (including those from @@ -38,12 +53,12 @@ class ExtractMayaAsciiRaw(pype.api.Extractor): members = instance[:] # Perform extraction - self.log.info("Performing extraction..") + self.log.info("Performing extraction ...") with avalon.maya.maintained_selection(): cmds.select(members, noExpand=True) cmds.file(path, force=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=True, constructionHistory=True, @@ -55,8 +70,8 @@ class ExtractMayaAsciiRaw(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'ma', - 'ext': 'ma', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path } diff --git a/pype/plugins/maya/publish/extract_model.py b/pype/plugins/maya/publish/extract_model.py index ba56194eea..330e471e53 100644 --- a/pype/plugins/maya/publish/extract_model.py +++ b/pype/plugins/maya/publish/extract_model.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract model as Maya Scene.""" import os from maya import cmds @@ -8,7 +10,7 @@ from pype.hosts.maya import lib class ExtractModel(pype.api.Extractor): - """Extract as Model (Maya Ascii) + """Extract as Model (Maya Scene). Only extracts contents based on the original "setMembers" data to ensure publishing the least amount of required shapes. From that it only takes @@ -22,19 +24,33 @@ class ExtractModel(pype.api.Extractor): """ - label = "Model (Maya ASCII)" + label = "Model (Maya Scene)" hosts = ["maya"] families = ["model"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass # Define extract output file path stagingdir = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) + filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(stagingdir, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.info("Performing extraction ...") # Get only the shape contents we need in such a way that we avoid # taking along intermediateObjects @@ -59,7 +75,7 @@ class ExtractModel(pype.api.Extractor): cmds.select(members, noExpand=True) cmds.file(path, force=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, channels=False, @@ -73,8 +89,8 @@ class ExtractModel(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'ma', - 'ext': 'ma', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': filename, "stagingDir": stagingdir, } diff --git a/pype/plugins/maya/publish/extract_rig.py b/pype/plugins/maya/publish/extract_rig.py index c98e562313..8ebeae4184 100644 --- a/pype/plugins/maya/publish/extract_rig.py +++ b/pype/plugins/maya/publish/extract_rig.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Extract rig as Maya Scene.""" import os from maya import cmds @@ -7,26 +9,40 @@ import pype.api class ExtractRig(pype.api.Extractor): - """Extract rig as Maya Ascii""" + """Extract rig as Maya Scene.""" - label = "Extract Rig (Maya ASCII)" + label = "Extract Rig (Maya Scene)" hosts = ["maya"] families = ["rig"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass # Define extract output file path dir_path = self.staging_dir(instance) - filename = "{0}.ma".format(instance.name) + filename = "{0}.{1}".format(instance.name, self.scene_type) path = os.path.join(dir_path, filename) # Perform extraction - self.log.info("Performing extraction..") + self.log.info("Performing extraction ...") with avalon.maya.maintained_selection(): cmds.select(instance, noExpand=True) cmds.file(path, force=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, preserveReferences=False, channels=True, @@ -38,12 +54,11 @@ class ExtractRig(pype.api.Extractor): instance.data["representations"] = [] representation = { - 'name': 'ma', - 'ext': 'ma', + 'name': self.scene_type, + 'ext': self.scene_type, 'files': filename, "stagingDir": dir_path } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) diff --git a/pype/plugins/maya/publish/extract_yeti_rig.py b/pype/plugins/maya/publish/extract_yeti_rig.py index d7bbcd6555..2f66d3e026 100644 --- a/pype/plugins/maya/publish/extract_yeti_rig.py +++ b/pype/plugins/maya/publish/extract_yeti_rig.py @@ -1,3 +1,6 @@ +# -*- coding: utf-8 -*- +"""Extract Yeti rig.""" + import os import json import contextlib @@ -11,7 +14,7 @@ import pype.hosts.maya.lib as maya @contextlib.contextmanager def disconnect_plugs(settings, members): - + """Disconnect and store attribute connections.""" members = cmds.ls(members, long=True) original_connections = [] try: @@ -55,7 +58,7 @@ def disconnect_plugs(settings, members): @contextlib.contextmanager def yetigraph_attribute_values(assumed_destination, resources): - + """Get values from Yeti attributes in graph.""" try: for resource in resources: if "graphnode" not in resource: @@ -89,14 +92,28 @@ def yetigraph_attribute_values(assumed_destination, resources): class ExtractYetiRig(pype.api.Extractor): - """Extract the Yeti rig to a MayaAscii and write the Yeti rig data""" + """Extract the Yeti rig to a Maya Scene and write the Yeti rig data.""" label = "Extract Yeti Rig" hosts = ["maya"] families = ["yetiRig"] + scene_type = "ma" def process(self, instance): - + """Plugin entry point.""" + ext_mapping = instance.context.data["presets"]["maya"].get("ext_mapping") # noqa: E501 + if ext_mapping: + self.log.info("Looking in presets for scene type ...") + # use extension mapping for first family found + for family in self.families: + try: + self.scene_type = ext_mapping[family] + self.log.info( + "Using {} as scene type".format(self.scene_type)) + break + except AttributeError: + # no preset found + pass yeti_nodes = cmds.ls(instance, type="pgYetiMaya") if not yeti_nodes: raise RuntimeError("No pgYetiMaya nodes found in the instance") @@ -106,7 +123,8 @@ class ExtractYetiRig(pype.api.Extractor): settings_path = os.path.join(dirname, "yeti.rigsettings") # Yeti related staging dirs - maya_path = os.path.join(dirname, "yeti_rig.ma") + maya_path = os.path.join( + dirname, "yeti_rig.{}".format(self.scene_type)) self.log.info("Writing metadata file") @@ -153,7 +171,7 @@ class ExtractYetiRig(pype.api.Extractor): cmds.file(maya_path, force=True, exportSelected=True, - typ="mayaAscii", + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 preserveReferences=False, constructionHistory=True, shader=False) @@ -163,21 +181,21 @@ class ExtractYetiRig(pype.api.Extractor): if "representations" not in instance.data: instance.data["representations"] = [] - self.log.info("rig file: {}".format("yeti_rig.ma")) + self.log.info("rig file: {}".format(maya_path)) instance.data["representations"].append( { - 'name': "ma", - 'ext': 'ma', - 'files': "yeti_rig.ma", + 'name': self.scene_type, + 'ext': self.scene_type, + 'files': os.path.basename(maya_path), 'stagingDir': dirname } ) - self.log.info("settings file: {}".format("yeti.rigsettings")) + self.log.info("settings file: {}".format(settings)) instance.data["representations"].append( { 'name': 'rigsettings', 'ext': 'rigsettings', - 'files': 'yeti.rigsettings', + 'files': os.path.basename(settings), 'stagingDir': dirname } ) From 7394e7284a7ccef06e9c2321088bc0cb7a314b25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Sun, 2 Aug 2020 23:08:55 +0200 Subject: [PATCH 30/40] disable undo/redo during extraction, fix frame num --- pype/hosts/harmony/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index 3cae695852..d1a9c3ae17 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -151,6 +151,7 @@ def application_launch(): def export_template(backdrops, nodes, filepath): func = """function func(args) { + scene.beginUndoRedoAccum("Publish: export template"); // Add an extra node just so a new group can be created. var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); var template_group = node.createGroup(temp_node, "temp_group"); @@ -168,7 +169,7 @@ def export_template(backdrops, nodes, filepath): }; // Copy-paste the selected nodes into the new group. - var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, ""); + var drag_object = copyPaste.copy(args[1], 1, frame.numberOf(), ""); copyPaste.pasteNewNodes(drag_object, template_group, ""); // Select all nodes within group and export as template. @@ -179,6 +180,7 @@ def export_template(backdrops, nodes, filepath): // created during the process. Action.perform("onActionUpToParent()", "Node View"); node.deleteNode(template_group, true, true); + scene.cancelUndoRedoAccum(); } func """ From 0f32a6d056a48c5102d07c023ea335f81224b63a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 4 Aug 2020 13:57:41 +0200 Subject: [PATCH 31/40] use Action.perform copy() to copy nodes --- pype/hosts/harmony/__init__.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index d1a9c3ae17..d4b7d91fdb 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -151,28 +151,31 @@ def application_launch(): def export_template(backdrops, nodes, filepath): func = """function func(args) { - scene.beginUndoRedoAccum("Publish: export template"); - // Add an extra node just so a new group can be created. + var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); var template_group = node.createGroup(temp_node, "temp_group"); node.deleteNode( template_group + "/temp_note" ); - // This will make Node View to focus on the new group. + selection.clearSelection(); + for (var f = 0; f < args[1].length; f++) + { + selection.addNodeToSelection(args[1][f]); + } + + Action.perform("copy()", "Node View"); + selection.clearSelection(); selection.addNodeToSelection(template_group); Action.perform("onActionEnterGroup()", "Node View"); + Action.perform("paste()", "Node View"); // Recreate backdrops in group. for (var i = 0 ; i < args[0].length; i++) { + MessageLog.trace(args[0][i]); Backdrop.addBackdrop(template_group, args[0][i]); }; - // Copy-paste the selected nodes into the new group. - var drag_object = copyPaste.copy(args[1], 1, frame.numberOf(), ""); - copyPaste.pasteNewNodes(drag_object, template_group, ""); - - // Select all nodes within group and export as template. Action.perform( "selectAll()", "Node View" ); copyPaste.createTemplateFromSelection(args[2], args[3]); @@ -180,7 +183,6 @@ def export_template(backdrops, nodes, filepath): // created during the process. Action.perform("onActionUpToParent()", "Node View"); node.deleteNode(template_group, true, true); - scene.cancelUndoRedoAccum(); } func """ From 9a4c7673e9244f8486655f2c7795f6f5b67553ae Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 4 Aug 2020 21:37:35 +0200 Subject: [PATCH 32/40] temporary change to extract render logic collection should eventually be phased out in favour of explicit render collection --- .../plugins/harmony/publish/extract_render.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index fe1352f9f9..45b52e0307 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -72,19 +72,27 @@ class ExtractRender(pyblish.api.InstancePlugin): self.log.info(output.decode("utf-8")) # Collect rendered files. + self.log.debug(path) files = os.listdir(path) + self.log.debug(files) collections, remainder = clique.assemble(files, minimum_items=1) assert not remainder, ( "There should not be a remainder for {0}: {1}".format( instance[0], remainder ) ) - assert len(collections) == 1, ( - "There should only be one image sequence in {}. Found: {}".format( - path, len(collections) - ) - ) - collection = collections[0] + self.log.debug(collections) + if len(collections) > 1: + for col in collections: + if len(list(col)) > 1: + collection = col + else: + # assert len(collections) == 1, ( + # "There should only be one image sequence in {}. Found: {}".format( + # path, len(collections) + # ) + # ) + collection = collections[0] # Generate thumbnail. thumbnail_path = os.path.join(path, "thumbnail.png") From 0c883a9f5b300874be3ba52849936e17ac09673a Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 4 Aug 2020 21:38:29 +0200 Subject: [PATCH 33/40] temp frame range filter implementation temporary implementation for a client. must be cleaned up and logic should be changed to work based on task type --- .../harmony/publish/validate_scene_settings.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pype/plugins/harmony/publish/validate_scene_settings.py b/pype/plugins/harmony/publish/validate_scene_settings.py index aa9a70bd85..3602f1ca22 100644 --- a/pype/plugins/harmony/publish/validate_scene_settings.py +++ b/pype/plugins/harmony/publish/validate_scene_settings.py @@ -28,8 +28,11 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): hosts = ["harmony"] actions = [ValidateSceneSettingsRepair] + frame_check_filter = ["_ch_", "_pr_", "_intd_", "_extd_"] + def process(self, instance): expected_settings = pype.hosts.harmony.get_asset_settings() + self.log.info(expected_settings) # Harmony is expected to start at 1. frame_start = expected_settings["frameStart"] @@ -37,6 +40,14 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): expected_settings["frameEnd"] = frame_end - frame_start + 1 expected_settings["frameStart"] = 1 + + + self.log.info(instance.context.data['anatomyData']['asset']) + + if any(string in instance.context.data['anatomyData']['asset'] + for string in frame_check_filter): + expected_settings.pop("frameEnd") + func = """function func() { return { From 5205e1773e4eb178baee3f6f000d17886a4a908c Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 4 Aug 2020 21:42:02 +0200 Subject: [PATCH 34/40] bump version --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 200c236308..ddcf716b76 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.11.1" +__version__ = "2.11.3" From 3770d642e92568515fe1c9cfb6790172c3a6e6a3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Aug 2020 10:57:49 +0200 Subject: [PATCH 35/40] use only keys in delivery with `{root` in value --- pype/modules/ftrack/actions/action_delivery.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index 06257f32d5..ce02f2054d 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -81,13 +81,15 @@ class Delivery(BaseAction): anatomy = Anatomy(project_name) new_anatomies = [] first = None - for key in (anatomy.templates.get("delivery") or {}): - new_anatomies.append({ - "label": key, - "value": key - }) - if first is None: - first = key + for key, template in (anatomy.templates.get("delivery") or {}).items(): + # Use only keys with `{root}` or `{root[*]}` in value + if "{root" in template: + new_anatomies.append({ + "label": key, + "value": key + }) + if first is None: + first = key skipped = False # Add message if there are any common components From def7fa7e08447a48cf76d3943b73e6c01121ba8e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Aug 2020 11:06:16 +0200 Subject: [PATCH 36/40] added check for template value that it is a string --- pype/modules/ftrack/actions/action_delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/actions/action_delivery.py b/pype/modules/ftrack/actions/action_delivery.py index ce02f2054d..231aebdf7a 100644 --- a/pype/modules/ftrack/actions/action_delivery.py +++ b/pype/modules/ftrack/actions/action_delivery.py @@ -83,7 +83,7 @@ class Delivery(BaseAction): first = None for key, template in (anatomy.templates.get("delivery") or {}).items(): # Use only keys with `{root}` or `{root[*]}` in value - if "{root" in template: + if isinstance(template, str) and "{root" in template: new_anatomies.append({ "label": key, "value": key From 5724f3e474cf59254d2d15c049adc761d300606b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 7 Aug 2020 12:18:54 +0200 Subject: [PATCH 37/40] using -g 1 (same as -intra) to set right keyframes of burning output --- pype/scripts/otio_burnin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pype/scripts/otio_burnin.py b/pype/scripts/otio_burnin.py index 104ff0255c..16e24757dd 100644 --- a/pype/scripts/otio_burnin.py +++ b/pype/scripts/otio_burnin.py @@ -528,6 +528,9 @@ def burnins_from_data( if pix_fmt: ffmpeg_args.append("-pix_fmt {}".format(pix_fmt)) + # Use group one (same as `-intra` argument, which is deprecated) + ffmpeg_args.append("-g 1") + ffmpeg_args_str = " ".join(ffmpeg_args) burnin.render( output_path, args=ffmpeg_args_str, overwrite=overwrite, **data From 941f6be192f46e7ba8c9042e61b73bf7875c0663 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 7 Aug 2020 12:39:47 +0200 Subject: [PATCH 38/40] query also avalon group in get_pype_attr --- pype/modules/ftrack/lib/avalon_sync.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index f78e617f3a..3548968628 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -81,10 +81,11 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None): def get_pype_attr(session, split_hierarchical=True): custom_attributes = [] hier_custom_attributes = [] + # TODO remove deprecated "avalon" group from query cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" - " where group.name = \"{}\"" + " where group.name in (\"{}\", \"avalon\")" ).format(CUST_ATTR_GROUP) all_avalon_attr = session.query(cust_attrs_query).all() for cust_attr in all_avalon_attr: From ce3271de02a082ce06aecbeea3a15acbf10f282d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 7 Aug 2020 12:41:53 +0200 Subject: [PATCH 39/40] query also pype group in ftrack attributes --- pype/modules/ftrack/lib/avalon_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index c5c9eb9054..885b9d25cc 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -70,7 +70,7 @@ def get_avalon_attr(session, split_hierarchical=True): cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" - " where group.name = \"avalon\"" + " where group.name in (\"avalon\", \"pype\")" ) all_avalon_attr = session.query(cust_attrs_query).all() for cust_attr in all_avalon_attr: From 23caec0a76d10b660f1a11d462e825d6f2163feb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 7 Aug 2020 13:45:32 +0200 Subject: [PATCH 40/40] fix case where staging dir is not set --- pype/plugins/global/publish/cleanup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pype/plugins/global/publish/cleanup.py b/pype/plugins/global/publish/cleanup.py index bca540078f..e891b7b7f6 100644 --- a/pype/plugins/global/publish/cleanup.py +++ b/pype/plugins/global/publish/cleanup.py @@ -71,12 +71,16 @@ class CleanUp(pyblish.api.InstancePlugin): temp_root = tempfile.gettempdir() staging_dir = instance.data.get("stagingDir", None) + if not staging_dir: + self.log.info("Staging dir not set.") + return + if not os.path.normpath(staging_dir).startswith(temp_root): self.log.info("Skipping cleanup. Staging directory is not in the " "temp folder: %s" % staging_dir) return - if not staging_dir or not os.path.exists(staging_dir): + if not os.path.exists(staging_dir): self.log.info("No staging directory found: %s" % staging_dir) return