diff --git a/pype/api.py b/pype/api.py index ae42bd99ba..5775bb3ce4 100644 --- a/pype/api.py +++ b/pype/api.py @@ -6,6 +6,12 @@ from pypeapp import ( execute ) +from pypeapp.lib.mongo import ( + decompose_url, + compose_url, + get_default_components +) + from .plugin import ( Extractor, @@ -30,6 +36,7 @@ from .lib import ( get_hierarchy, get_subsets, get_version_from_path, + get_last_version_from_path, modified_environ, add_tool_to_environment ) @@ -43,6 +50,9 @@ __all__ = [ "project_overrides_dir_path", "config", "execute", + "decompose_url", + "compose_url", + "get_default_components", # plugin classes "Extractor", @@ -67,6 +77,7 @@ __all__ = [ "get_asset", "get_subsets", "get_version_from_path", + "get_last_version_from_path", "modified_environ", "add_tool_to_environment", diff --git a/pype/hooks/celaction/prelaunch.py b/pype/hooks/celaction/prelaunch.py new file mode 100644 index 0000000000..df9da6cbbf --- /dev/null +++ b/pype/hooks/celaction/prelaunch.py @@ -0,0 +1,208 @@ +import logging +import os +import winreg +import shutil +from pype.lib import PypeHook +from pype.api import ( + Anatomy, + Logger, + get_last_version_from_path +) + +from avalon import io, api, lib + +log = logging.getLogger(__name__) + + +class CelactionPrelaunchHook(PypeHook): + """ + This hook will check if current workfile path has Unreal + project inside. IF not, it initialize it and finally it pass + path to the project by environment variable to Unreal launcher + shell script. + """ + workfile_ext = "scn" + + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + if not env: + env = os.environ + + # initialize + self._S = api.Session + + # get publish version of celaction + app = "celaction_publish" + + # get context variables + project = self._S["AVALON_PROJECT"] = env["AVALON_PROJECT"] + asset = self._S["AVALON_ASSET"] = env["AVALON_ASSET"] + task = self._S["AVALON_TASK"] = env["AVALON_TASK"] + workdir = self._S["AVALON_WORKDIR"] = env["AVALON_WORKDIR"] + + # get workfile path + anatomy_filled = self.get_anatomy_filled() + workfile = anatomy_filled["work"]["file"] + version = anatomy_filled["version"] + + # create workdir if doesn't exist + os.makedirs(workdir, exist_ok=True) + self.log.info(f"Work dir is: `{workdir}`") + + # get last version of workfile + workfile_last = get_last_version_from_path( + workdir, workfile.split(version)) + + if workfile_last: + workfile = workfile_last + + workfile_path = os.path.join(workdir, workfile) + + # copy workfile from template if doesnt exist any on path + if not os.path.isfile(workfile_path): + # try to get path from environment or use default + # from `pype.celation` dir + template_path = env.get("CELACTION_TEMPLATE") or os.path.join( + env.get("PYPE_MODULE_ROOT"), + "pype/hosts/celaction/celaction_template_scene.scn" + ) + self.log.info( + f"Creating workfile from template: `{template_path}`") + shutil.copy2( + os.path.normpath(template_path), + os.path.normpath(workfile_path) + ) + + self.log.info(f"Workfile to open: `{workfile_path}`") + + # adding compulsory environment var for openting file + env["PYPE_CELACTION_PROJECT_FILE"] = workfile_path + + # setting output parameters + path = r"Software\CelAction\CelAction2D\User Settings" + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + "Software\\CelAction\\CelAction2D\\User Settings", 0, + winreg.KEY_ALL_ACCESS) + + # TODO: change to root path and pyblish standalone to premiere way + pype_root_path = os.getenv("PYPE_SETUP_PATH") + path = os.path.join(pype_root_path, + "pype.bat") + + winreg.SetValueEx(hKey, "SubmitAppTitle", 0, winreg.REG_SZ, path) + + parameters = [ + "launch", + f"--app {app}", + f"--project {project}", + f"--asset {asset}", + f"--task {task}", + "--currentFile \"*SCENE*\"", + "--chunk *CHUNK*", + "--frameStart *START*", + "--frameEnd *END*", + "--resolutionWidth *X*", + "--resolutionHeight *Y*", + # "--programDir \"'*PROGPATH*'\"" + ] + winreg.SetValueEx(hKey, "SubmitParametersTitle", 0, winreg.REG_SZ, + " ".join(parameters)) + + # setting resolution parameters + path = r"Software\CelAction\CelAction2D\User Settings\Dialogs" + path += r"\SubmitOutput" + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "SaveScene", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(hKey, "CustomX", 0, winreg.REG_DWORD, 1920) + winreg.SetValueEx(hKey, "CustomY", 0, winreg.REG_DWORD, 1080) + + # making sure message dialogs don't appear when overwriting + path = r"Software\CelAction\CelAction2D\User Settings\Messages" + path += r"\OverwriteScene" + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 6) + winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) + + path = r"Software\CelAction\CelAction2D\User Settings\Messages" + path += r"\SceneSaved" + winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) + hKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, + winreg.KEY_ALL_ACCESS) + winreg.SetValueEx(hKey, "Result", 0, winreg.REG_DWORD, 1) + winreg.SetValueEx(hKey, "Valid", 0, winreg.REG_DWORD, 1) + + return True + + def get_anatomy_filled(self): + root_path = api.registered_root() + project_name = self._S["AVALON_PROJECT"] + asset_name = self._S["AVALON_ASSET"] + + io.install() + project_entity = io.find_one({ + "type": "project", + "name": project_name + }) + assert project_entity, ( + "Project '{0}' was not found." + ).format(project_name) + log.debug("Collected Project \"{}\"".format(project_entity)) + + asset_entity = io.find_one({ + "type": "asset", + "name": asset_name, + "parent": project_entity["_id"] + }) + assert asset_entity, ( + "No asset found by the name '{0}' in project '{1}'" + ).format(asset_name, project_name) + + project_name = project_entity["name"] + + log.info( + "Anatomy object collected for project \"{}\".".format(project_name) + ) + + hierarchy_items = asset_entity["data"]["parents"] + hierarchy = "" + if hierarchy_items: + hierarchy = os.path.join(*hierarchy_items) + + template_data = { + "root": root_path, + "project": { + "name": project_name, + "code": project_entity["data"].get("code") + }, + "asset": asset_entity["name"], + "hierarchy": hierarchy.replace("\\", "/"), + "task": self._S["AVALON_TASK"], + "ext": self.workfile_ext, + "version": 1, + "username": os.getenv("PYPE_USERNAME", "").strip() + } + + avalon_app_name = os.environ.get("AVALON_APP_NAME") + if avalon_app_name: + application_def = lib.get_application(avalon_app_name) + app_dir = application_def.get("application_dir") + if app_dir: + template_data["app"] = app_dir + + anatomy = Anatomy(project_name) + anatomy_filled = anatomy.format_all(template_data).get_solved() + + return anatomy_filled diff --git a/pype/hooks/photoshop/prelaunch.py b/pype/hooks/photoshop/prelaunch.py new file mode 100644 index 0000000000..4f00e4cd83 --- /dev/null +++ b/pype/hooks/photoshop/prelaunch.py @@ -0,0 +1,23 @@ +import pype.lib +from pype.api import Logger + + +class PhotoshopPrelaunch(pype.lib.PypeHook): + """This hook will check for the existence of PyWin + + PyWin is a requirement for the Photoshop integration. + """ + project_code = None + + def __init__(self, logger=None): + if not logger: + self.log = Logger().get_logger(self.__class__.__name__) + else: + self.log = logger + + self.signature = "( {} )".format(self.__class__.__name__) + + def execute(self, *args, env: dict = None) -> bool: + output = pype.lib._subprocess(["pip", "install", "pywin32==227"]) + self.log.info(output) + return True diff --git a/pype/hosts/celaction/__init__.py b/pype/hosts/celaction/__init__.py new file mode 100644 index 0000000000..8c93d93738 --- /dev/null +++ b/pype/hosts/celaction/__init__.py @@ -0,0 +1 @@ +kwargs = None diff --git a/pype/hosts/celaction/celaction_template_scene.scn b/pype/hosts/celaction/celaction_template_scene.scn new file mode 100644 index 0000000000..54e4497a31 Binary files /dev/null and b/pype/hosts/celaction/celaction_template_scene.scn differ diff --git a/pype/hosts/celaction/cli.py b/pype/hosts/celaction/cli.py new file mode 100644 index 0000000000..fa55db3200 --- /dev/null +++ b/pype/hosts/celaction/cli.py @@ -0,0 +1,121 @@ +import os +import sys +import copy +import argparse + +from avalon import io +from avalon.tools import publish + +import pyblish.api +import pyblish.util + +from pype.api import Logger +import pype +import pype.celaction + +log = Logger().get_logger("Celaction_cli_publisher") + +publish_host = "celaction" + +PUBLISH_PATH = os.path.join(pype.PLUGINS_DIR, publish_host, "publish") + +PUBLISH_PATHS = [ + PUBLISH_PATH, + os.path.join(pype.PLUGINS_DIR, "ftrack", "publish") +] + + +def cli(): + parser = argparse.ArgumentParser(prog="celaction_publish") + + parser.add_argument("--currentFile", + help="Pass file to Context as `currentFile`") + + parser.add_argument("--chunk", + help=("Render chanks on farm")) + + parser.add_argument("--frameStart", + help=("Start of frame range")) + + parser.add_argument("--frameEnd", + help=("End of frame range")) + + parser.add_argument("--resolutionWidth", + help=("Width of resolution")) + + parser.add_argument("--resolutionHeight", + help=("Height of resolution")) + + # parser.add_argument("--programDir", + # help=("Directory with celaction program installation")) + + pype.celaction.kwargs = parser.parse_args(sys.argv[1:]).__dict__ + + +def _prepare_publish_environments(): + """Prepares environments based on request data.""" + env = copy.deepcopy(os.environ) + + project_name = os.getenv("AVALON_PROJECT") + asset_name = os.getenv("AVALON_ASSET") + + io.install() + project_doc = io.find_one({ + "type": "project" + }) + av_asset = io.find_one({ + "type": "asset", + "name": asset_name + }) + parents = av_asset["data"]["parents"] + hierarchy = "" + if parents: + hierarchy = "/".join(parents) + + env["AVALON_PROJECT"] = project_name + env["AVALON_ASSET"] = asset_name + env["AVALON_TASK"] = os.getenv("AVALON_TASK") + env["AVALON_WORKDIR"] = os.getenv("AVALON_WORKDIR") + env["AVALON_HIERARCHY"] = hierarchy + env["AVALON_PROJECTCODE"] = project_doc["data"].get("code", "") + env["AVALON_APP"] = publish_host + env["AVALON_APP_NAME"] = "celaction_local" + + env["PYBLISH_HOSTS"] = publish_host + + os.environ.update(env) + + +def main(): + # prepare all environments + _prepare_publish_environments() + + # Registers pype's Global pyblish plugins + pype.install() + + for path in PUBLISH_PATHS: + path = os.path.normpath(path) + + if not os.path.exists(path): + continue + + log.info(f"Registering path: {path}") + pyblish.api.register_plugin_path(path) + + pyblish.api.register_host(publish_host) + + # Register project specific plugins + project_name = os.environ["AVALON_PROJECT"] + project_plugins_paths = os.getenv("PYPE_PROJECT_PLUGINS", "") + for path in project_plugins_paths.split(os.pathsep): + plugin_path = os.path.join(path, project_name, "plugins") + if os.path.exists(plugin_path): + pyblish.api.register_plugin_path(plugin_path) + + return publish.show() + + +if __name__ == "__main__": + cli() + result = main() + sys.exit(not bool(result)) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index b3edca7d15..3d49c60563 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -1,14 +1,149 @@ import os +import sys from avalon import api, harmony +from avalon.vendor import Qt import pyblish.api +from pype import lib + + +def set_scene_settings(settings): + func = """function func(args) + { + if (args[0]["fps"]) + { + scene.setFrameRate(args[0]["fps"]); + } + if (args[0]["frameStart"] && args[0]["frameEnd"]) + { + var duration = args[0]["frameEnd"] - args[0]["frameStart"] + 1 + if (frame.numberOf() > duration) + { + frame.remove( + duration, frame.numberOf() - duration + ); + } + if (frame.numberOf() < duration) + { + frame.insert( + duration, duration - frame.numberOf() + ); + } + + scene.setStartFrame(1); + scene.setStopFrame(duration); + } + if (args[0]["resolutionWidth"] && args[0]["resolutionHeight"]) + { + scene.setDefaultResolution( + args[0]["resolutionWidth"], args[0]["resolutionHeight"], 41.112 + ) + } + } + func + """ + harmony.send({"function": func, "args": [settings]}) + + +def get_asset_settings(): + asset_data = lib.get_asset()["data"] + fps = asset_data.get("fps") + frame_start = asset_data.get("frameStart") + frame_end = asset_data.get("frameEnd") + resolution_width = asset_data.get("resolutionWidth") + resolution_height = asset_data.get("resolutionHeight") + + return { + "fps": fps, + "frameStart": frame_start, + "frameEnd": frame_end, + "resolutionWidth": resolution_width, + "resolutionHeight": resolution_height + } + + +def ensure_scene_settings(): + settings = get_asset_settings() + + invalid_settings = [] + valid_settings = {} + for key, value in settings.items(): + if value is None: + invalid_settings.append(key) + else: + valid_settings[key] = value + + # Warn about missing attributes. + print("Starting new QApplication..") + app = Qt.QtWidgets.QApplication(sys.argv) + + message_box = Qt.QtWidgets.QMessageBox() + message_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg = "Missing attributes:" + if invalid_settings: + for item in invalid_settings: + msg += f"\n{item}" + message_box.setText(msg) + message_box.exec_() + + # Garbage collect QApplication. + del app + + set_scene_settings(valid_settings) + + +def export_template(backdrops, nodes, filepath): + func = """function func(args) + { + // 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(); + selection.addNodeToSelection(template_group); + Action.perform("onActionEnterGroup()", "Node View"); + + // Recreate backdrops in group. + for (var i = 0 ; i < args[0].length; 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]); + + // Unfocus the group in Node view, delete all nodes and backdrops + // created during the process. + Action.perform("onActionUpToParent()", "Node View"); + node.deleteNode(template_group, true, true); + } + func + """ + harmony.send({ + "function": func, + "args": [ + backdrops, + nodes, + os.path.basename(filepath), + os.path.dirname(filepath) + ] + }) def install(): print("Installing Pype config...") plugins_directory = os.path.join( - os.path.dirname(os.path.dirname(__file__)), "plugins", "harmony" + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "plugins", + "harmony" ) pyblish.api.register_plugin_path( @@ -21,10 +156,13 @@ def install(): api.Creator, os.path.join(plugins_directory, "create") ) + # Register callbacks. pyblish.api.register_callback( "instanceToggled", on_pyblish_instance_toggled ) + api.on("application.launched", ensure_scene_settings) + def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle node enabling on instance toggles.""" diff --git a/pype/lib.py b/pype/lib.py index d76d02ea5a..87808e53f5 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -17,6 +17,7 @@ import six import avalon.api from .api import config + log = logging.getLogger(__name__) @@ -469,6 +470,43 @@ def get_version_from_path(file): ) +def get_last_version_from_path(path_dir, filter): + """ + Finds last version of given directory content + + Args: + path_dir (string): directory path + filter (list): list of strings used as file name filter + + Returns: + string: file name with last version + + Example: + last_version_file = get_last_version_from_path( + "/project/shots/shot01/work", ["shot01", "compositing", "nk"]) + """ + + assert os.path.isdir(path_dir), "`path_dir` argument needs to be directory" + assert isinstance(filter, list) and ( + len(filter) != 0), "`filter` argument needs to be list and not empty" + + filtred_files = list() + + # form regex for filtering + patern = r".*".join(filter) + + for f in os.listdir(path_dir): + if not re.findall(patern, f): + continue + filtred_files.append(f) + + if filtred_files: + sorted(filtred_files) + return filtred_files[-1] + else: + return None + + def get_avalon_database(): if io._database is None: set_io_database() @@ -610,7 +648,7 @@ def get_subsets(asset_name, if len(repres_out) > 0: output_dict[subset["name"]] = {"version": version_sel, - "representaions": repres_out} + "representations": repres_out} return output_dict diff --git a/pype/modules/adobe_communicator/lib/io_nonsingleton.py b/pype/modules/adobe_communicator/lib/io_nonsingleton.py index 6380e4eb23..da37c657c6 100644 --- a/pype/modules/adobe_communicator/lib/io_nonsingleton.py +++ b/pype/modules/adobe_communicator/lib/io_nonsingleton.py @@ -16,6 +16,7 @@ import contextlib from avalon import schema from avalon.vendor import requests +from avalon.io import extract_port_from_url # Third-party dependencies import pymongo @@ -72,8 +73,17 @@ class DbConnector(object): self.Session.update(self._from_environment()) timeout = int(self.Session["AVALON_TIMEOUT"]) - self._mongo_client = pymongo.MongoClient( - self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout) + mongo_url = self.Session["AVALON_MONGO"] + kwargs = { + "host": mongo_url, + "serverSelectionTimeoutMS": timeout + } + + port = extract_port_from_url(mongo_url) + if port is not None: + kwargs["port"] = int(port) + + self._mongo_client = pymongo.MongoClient(**kwargs) for retry in range(3): try: @@ -381,6 +391,10 @@ class DbConnector(object): if document is None: break + if document.get("type") == "master_version": + _document = self.find_one({"_id": document["version_id"]}) + document["data"] = _document["data"] + parents.append(document) return parents diff --git a/pype/modules/avalon_apps/rest_api.py b/pype/modules/avalon_apps/rest_api.py index a5dc326a8e..1cb9e544a7 100644 --- a/pype/modules/avalon_apps/rest_api.py +++ b/pype/modules/avalon_apps/rest_api.py @@ -4,17 +4,14 @@ import json import bson import bson.json_util from pype.modules.rest_api import RestApi, abort, CallbackResult -from pype.modules.ftrack.lib.custom_db_connector import DbConnector +from pype.modules.ftrack.lib.io_nonsingleton import DbConnector class AvalonRestApi(RestApi): - dbcon = DbConnector( - os.environ["AVALON_MONGO"], - os.environ["AVALON_DB"] - ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.dbcon = DbConnector() self.dbcon.install() @RestApi.route("/projects/", url_prefix="/avalon", methods="GET") diff --git a/pype/modules/ftrack/actions/action_batch_task_creation.py b/pype/modules/ftrack/actions/action_batch_task_creation.py new file mode 100644 index 0000000000..ef370d55eb --- /dev/null +++ b/pype/modules/ftrack/actions/action_batch_task_creation.py @@ -0,0 +1,164 @@ +""" +Taken from https://github.com/tokejepsen/ftrack-hooks/tree/master/batch_tasks +""" + +from pype.modules.ftrack.lib import BaseAction + + +class BatchTasksAction(BaseAction): + '''Batch Tasks action + `label` a descriptive string identifing your action. + `varaint` To group actions together, give them the same + label and specify a unique variant per action. + `identifier` a unique identifier for your action. + `description` a verbose descriptive text for you action + ''' + label = "Batch Tasks" + variant = None + identifier = "batch-tasks" + description = None + + def discover(self, session, entities, event): + '''Return true if we can handle the selected entities. + *session* is a `ftrack_api.Session` instance + *entities* is a list of tuples each containing the entity type and the + entity id. + If the entity is a hierarchical you will always get the entity + type TypedContext, once retrieved through a get operation you + will have the "real" entity type ie. example Shot, Sequence + or Asset Build. + *event* the unmodified original event + ''' + # Only discover the action if any selection is made. + if entities: + return True + + return False + + def get_task_form_items(self, session, number_of_tasks): + items = [] + + task_type_options = [ + {'label': task_type["name"], 'value': task_type["id"]} + for task_type in session.query("Type") + ] + + for index in range(0, number_of_tasks): + items.extend( + [ + { + 'value': '##Template for Task{0}##'.format( + index + ), + 'type': 'label' + }, + { + 'label': 'Type', + 'type': 'enumerator', + 'name': 'task_{0}_typeid'.format(index), + 'data': task_type_options + }, + { + 'label': 'Name', + 'type': 'text', + 'name': 'task_{0}_name'.format(index) + } + ] + ) + + return items + + def ensure_task(self, session, name, task_type, parent): + + # Query for existing task. + query = ( + 'Task where type.id is "{0}" and name is "{1}" ' + 'and parent.id is "{2}"' + ) + task = session.query( + query.format( + task_type["id"], + name, + parent["id"] + ) + ).first() + + # Create task. + if not task: + session.create( + "Task", + { + "name": name, + "type": task_type, + "parent": parent + } + ) + + def launch(self, session, entities, event): + '''Callback method for the custom action. + return either a bool ( True if successful or False if the action + failed ) or a dictionary with they keys `message` and `success`, the + message should be a string and will be displayed as feedback to the + user, success should be a bool, True if successful or False if the + action failed. + *session* is a `ftrack_api.Session` instance + *entities* is a list of tuples each containing the entity type and the + entity id. + If the entity is a hierarchical you will always get the entity + type TypedContext, once retrieved through a get operation you + will have the "real" entity type ie. example Shot, Sequence + or Asset Build. + *event* the unmodified original event + ''' + if 'values' in event['data']: + values = event['data']['values'] + if 'number_of_tasks' in values: + return { + 'success': True, + 'message': '', + 'items': self.get_task_form_items( + session, int(values['number_of_tasks']) + ) + } + else: + # Create tasks on each entity + for entity in entities: + for count in range(0, int(len(values.keys()) / 2)): + task_type = session.query( + 'Type where id is "{0}"'.format( + values["task_{0}_typeid".format(count)] + ) + ).one() + + # Get name, or assume task type in lower case as name. + name = values["task_{0}_name".format(count)] + if not name: + name = task_type["name"].lower() + + self.ensure_task(session, name, task_type, entity) + + session.commit() + + return { + 'success': True, + 'message': 'Action completed successfully' + } + + return { + 'success': True, + 'message': "", + 'items': [ + { + 'label': 'Number of tasks', + 'type': 'number', + 'name': 'number_of_tasks', + 'value': 2 + } + ] + } + + +def register(session, plugins_presets=None): + '''Register action. Called when used as an event plugin.''' + + BatchTasksAction(session, plugins_presets).register() diff --git a/pype/modules/ftrack/events/event_first_version_status.py b/pype/modules/ftrack/events/event_first_version_status.py index 511907a048..8754d092ab 100644 --- a/pype/modules/ftrack/events/event_first_version_status.py +++ b/pype/modules/ftrack/events/event_first_version_status.py @@ -158,7 +158,7 @@ class FirstVersionStatus(BaseEvent): filtered_ents = [] for entity in event["data"].get("entities", []): # Care only about add actions - if entity["action"] != "add": + if entity.get("action") != "add": continue # Filter AssetVersions diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index e79fcac4cd..739ec69522 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -45,6 +45,7 @@ class SyncToAvalonEvent(BaseEvent): " where project_id is \"{}\" and name in ({})" ) created_entities = [] + report_splitter = {"type": "label", "value": "---"} def __init__(self, session, plugins_presets={}): '''Expects a ftrack_api.Session instance''' diff --git a/pype/modules/ftrack/events/event_thumbnail_updates.py b/pype/modules/ftrack/events/event_thumbnail_updates.py index c33ee08c2d..1304dc8b5e 100644 --- a/pype/modules/ftrack/events/event_thumbnail_updates.py +++ b/pype/modules/ftrack/events/event_thumbnail_updates.py @@ -6,6 +6,9 @@ class ThumbnailEvents(BaseEvent): """Updates thumbnails of entities from new AssetVersion.""" for entity in event["data"].get("entities", []): + action = entity.get("action") + if not action: + continue if ( entity["action"] == "remove" or entity["entityType"].lower() != "assetversion" diff --git a/pype/modules/ftrack/ftrack_server/event_server_cli.py b/pype/modules/ftrack/ftrack_server/event_server_cli.py index 5709a88297..73c7abfc5d 100644 --- a/pype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/pype/modules/ftrack/ftrack_server/event_server_cli.py @@ -13,10 +13,12 @@ import time import uuid import ftrack_api +import pymongo from pype.modules.ftrack.lib import credentials from pype.modules.ftrack.ftrack_server.lib import ( - ftrack_events_mongo_settings, check_ftrack_url + check_ftrack_url, get_ftrack_event_mongo_info ) + import socket_thread @@ -30,22 +32,19 @@ class MongoPermissionsError(Exception): def check_mongo_url(host, port, log_error=False): """Checks if mongo server is responding""" - sock = None try: - sock = socket.create_connection( - (host, port), - timeout=1 - ) - return True - except socket.error as err: + client = pymongo.MongoClient(host=host, port=port) + # Force connection on a request as the connect=True parameter of + # MongoClient seems to be useless here + client.server_info() + except pymongo.errors.ServerSelectionTimeoutError as err: if log_error: print("Can't connect to MongoDB at {}:{} because: {}".format( host, port, err )) return False - finally: - if sock is not None: - sock.close() + + return True def validate_credentials(url, user, api): @@ -190,9 +189,10 @@ def main_loop(ftrack_url): os.environ["FTRACK_EVENT_SUB_ID"] = str(uuid.uuid1()) # Get mongo hostname and port for testing mongo connection - mongo_list = ftrack_events_mongo_settings() - mongo_hostname = mongo_list[0] - mongo_port = mongo_list[1] + + mongo_uri, mongo_port, database_name, collection_name = ( + get_ftrack_event_mongo_info() + ) # Current file file_path = os.path.dirname(os.path.realpath(__file__)) @@ -270,13 +270,12 @@ def main_loop(ftrack_url): ftrack_accessible = check_ftrack_url(ftrack_url) if not mongo_accessible: - mongo_accessible = check_mongo_url(mongo_hostname, mongo_port) + mongo_accessible = check_mongo_url(mongo_uri, mongo_port) # Run threads only if Ftrack is accessible if not ftrack_accessible or not mongo_accessible: if not mongo_accessible and not printed_mongo_error: - mongo_url = mongo_hostname + ":" + mongo_port - print("Can't access Mongo {}".format(mongo_url)) + print("Can't access Mongo {}".format(mongo_uri)) if not ftrack_accessible and not printed_ftrack_error: print("Can't access Ftrack {}".format(ftrack_url)) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 129cd7173a..327fab817d 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -18,12 +18,13 @@ import ftrack_api.operation import ftrack_api._centralized_storage_scenario import ftrack_api.event from ftrack_api.logging import LazyLogMessage as L -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs -from pype.api import Logger +from pype.api import ( + Logger, + get_default_components, + decompose_url, + compose_url +) from pype.modules.ftrack.lib.custom_db_connector import DbConnector @@ -32,69 +33,29 @@ TOPIC_STATUS_SERVER = "pype.event.server.status" TOPIC_STATUS_SERVER_RESULT = "pype.event.server.status.result" -def ftrack_events_mongo_settings(): - host = None - port = None - username = None - password = None - collection = None - database = None - auth_db = "" - - if os.environ.get('FTRACK_EVENTS_MONGO_URL'): - result = urlparse(os.environ['FTRACK_EVENTS_MONGO_URL']) - - host = result.hostname - try: - port = result.port - except ValueError: - raise RuntimeError("invalid port specified") - username = result.username - password = result.password - try: - database = result.path.lstrip("/").split("/")[0] - collection = result.path.lstrip("/").split("/")[1] - except IndexError: - if not database: - raise RuntimeError("missing database name for logging") - try: - auth_db = parse_qs(result.query)['authSource'][0] - except KeyError: - # no auth db provided, mongo will use the one we are connecting to - pass - else: - host = os.environ.get('FTRACK_EVENTS_MONGO_HOST') - port = int(os.environ.get('FTRACK_EVENTS_MONGO_PORT', "0")) - database = os.environ.get('FTRACK_EVENTS_MONGO_DB') - username = os.environ.get('FTRACK_EVENTS_MONGO_USER') - password = os.environ.get('FTRACK_EVENTS_MONGO_PASSWORD') - collection = os.environ.get('FTRACK_EVENTS_MONGO_COL') - auth_db = os.environ.get('FTRACK_EVENTS_MONGO_AUTH_DB', 'avalon') - - return host, port, database, username, password, collection, auth_db - - def get_ftrack_event_mongo_info(): - host, port, database, username, password, collection, auth_db = ( - ftrack_events_mongo_settings() + database_name = ( + os.environ.get("FTRACK_EVENTS_MONGO_DB") or "pype" + ) + collection_name = ( + os.environ.get("FTRACK_EVENTS_MONGO_COL") or "ftrack_events" ) - user_pass = "" - if username and password: - user_pass = "{}:{}@".format(username, password) - socket_path = "{}:{}".format(host, port) + mongo_url = os.environ.get("FTRACK_EVENTS_MONGO_URL") + if mongo_url is not None: + components = decompose_url(mongo_url) + _used_ftrack_url = True + else: + components = get_default_components() + _used_ftrack_url = False - dab = "" - if database: - dab = "/{}".format(database) + if not _used_ftrack_url or components["database"] is None: + components["database"] = database_name + components["collection"] = collection_name - auth = "" - if auth_db: - auth = "?authSource={}".format(auth_db) + uri = compose_url(components) - url = "mongodb://{}{}{}{}".format(user_pass, socket_path, dab, auth) - - return url, database, collection + return uri, components["port"], database_name, collection_name def check_ftrack_url(url, log_errors=True): @@ -198,16 +159,17 @@ class StorerEventHub(SocketBaseEventHub): class ProcessEventHub(SocketBaseEventHub): hearbeat_msg = b"processor" - url, database, table_name = get_ftrack_event_mongo_info() + uri, port, database, table_name = get_ftrack_event_mongo_info() is_table_created = False pypelog = Logger().get_logger("Session Processor") def __init__(self, *args, **kwargs): self.dbcon = DbConnector( - mongo_url=self.url, - database_name=self.database, - table_name=self.table_name + self.uri, + self.port, + self.database, + self.table_name ) super(ProcessEventHub, self).__init__(*args, **kwargs) @@ -269,7 +231,7 @@ class ProcessEventHub(SocketBaseEventHub): def load_events(self): """Load not processed events sorted by stored date""" ago_date = datetime.datetime.now() - datetime.timedelta(days=3) - result = self.dbcon.delete_many({ + self.dbcon.delete_many({ "pype_data.stored": {"$lte": ago_date}, "pype_data.is_processed": True }) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py index c4d199407d..61b9aaf2c8 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py @@ -23,12 +23,8 @@ class SessionFactory: session = None -url, database, table_name = get_ftrack_event_mongo_info() -dbcon = DbConnector( - mongo_url=url, - database_name=database, - table_name=table_name -) +uri, port, database, table_name = get_ftrack_event_mongo_info() +dbcon = DbConnector(uri, port, database, table_name) # ignore_topics = ["ftrack.meta.connected"] ignore_topics = [] diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index b307117127..a734b3f80a 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -12,6 +12,7 @@ import atexit # Third-party dependencies import pymongo +from pype.api import decompose_url class NotActiveTable(Exception): @@ -63,13 +64,29 @@ class DbConnector: log = logging.getLogger(__name__) timeout = 1000 - def __init__(self, mongo_url, database_name, table_name=None): + def __init__( + self, uri, port=None, database_name=None, table_name=None + ): self._mongo_client = None self._sentry_client = None self._sentry_logging_handler = None self._database = None self._is_installed = False - self._mongo_url = mongo_url + + self._uri = uri + components = decompose_url(uri) + if port is None: + port = components.get("port") + + if database_name is None: + database_name = components.get("database") + + if database_name is None: + raise ValueError( + "Database is not defined for connection. {}".format(uri) + ) + + self._port = port self._database_name = database_name self.active_table = table_name @@ -95,10 +112,16 @@ class DbConnector: atexit.register(self.uninstall) logging.basicConfig() - self._mongo_client = pymongo.MongoClient( - self._mongo_url, - serverSelectionTimeoutMS=self.timeout - ) + kwargs = { + "host": self._uri, + "serverSelectionTimeoutMS": self.timeout + } + if self._port is not None: + kwargs["port"] = self._port + + self._mongo_client = pymongo.MongoClient(**kwargs) + if self._port is None: + self._port = self._mongo_client.PORT for retry in range(3): try: @@ -113,11 +136,11 @@ class DbConnector: else: raise IOError( "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self._mongo_url, self.timeout) + "less than %.3f ms" % (self._uri, self.timeout) ) self.log.info("Connected to %s, delay %.3f s" % ( - self._mongo_url, time.time() - t1 + self._uri, time.time() - t1 )) self._database = self._mongo_client[self._database_name] diff --git a/pype/modules/ftrack/lib/ftrack_base_handler.py b/pype/modules/ftrack/lib/ftrack_base_handler.py index c7144bb2f7..ce6607d6bf 100644 --- a/pype/modules/ftrack/lib/ftrack_base_handler.py +++ b/pype/modules/ftrack/lib/ftrack_base_handler.py @@ -36,7 +36,7 @@ class BaseHandler(object): ignore_me = False preactions = [] - def __init__(self, session, plugins_presets={}): + def __init__(self, session, plugins_presets=None): '''Expects a ftrack_api.Session instance''' self.log = Logger().get_logger(self.__class__.__name__) if not( @@ -57,6 +57,8 @@ class BaseHandler(object): # Using decorator self.register = self.register_decorator(self.register) self.launch = self.launch_log(self.launch) + if plugins_presets is None: + plugins_presets = {} self.plugins_presets = plugins_presets # Decorator diff --git a/pype/modules/ftrack/lib/io_nonsingleton.py b/pype/modules/ftrack/lib/io_nonsingleton.py index 6380e4eb23..da37c657c6 100644 --- a/pype/modules/ftrack/lib/io_nonsingleton.py +++ b/pype/modules/ftrack/lib/io_nonsingleton.py @@ -16,6 +16,7 @@ import contextlib from avalon import schema from avalon.vendor import requests +from avalon.io import extract_port_from_url # Third-party dependencies import pymongo @@ -72,8 +73,17 @@ class DbConnector(object): self.Session.update(self._from_environment()) timeout = int(self.Session["AVALON_TIMEOUT"]) - self._mongo_client = pymongo.MongoClient( - self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout) + mongo_url = self.Session["AVALON_MONGO"] + kwargs = { + "host": mongo_url, + "serverSelectionTimeoutMS": timeout + } + + port = extract_port_from_url(mongo_url) + if port is not None: + kwargs["port"] = int(port) + + self._mongo_client = pymongo.MongoClient(**kwargs) for retry in range(3): try: @@ -381,6 +391,10 @@ class DbConnector(object): if document is None: break + if document.get("type") == "master_version": + _document = self.find_one({"_id": document["version_id"]}) + document["data"] = _document["data"] + parents.append(document) return parents diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index 484fd6dc69..ce1fa236a9 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,8 +1,7 @@ -import os import collections from Qt import QtCore from pype.api import Logger -from pypeapp.lib.log import _bootstrap_mongo_log +from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME log = Logger().get_logger("LogModel", "LoggingModule") @@ -41,11 +40,11 @@ class LogModel(QtCore.QAbstractItemModel): super(LogModel, self).__init__(parent) self._root_node = Node() - collection = os.environ.get('PYPE_LOG_MONGO_COL') - database = _bootstrap_mongo_log() self.dbcon = None - if collection in database.list_collection_names(): - self.dbcon = database[collection] + # 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) diff --git a/pype/modules/logging/tray/logging_module.py b/pype/modules/logging/tray/logging_module.py index 087a51f322..9b26d5d9bf 100644 --- a/pype/modules/logging/tray/logging_module.py +++ b/pype/modules/logging/tray/logging_module.py @@ -1,20 +1,23 @@ from Qt import QtWidgets - from pype.api import Logger - from ..gui.app import LogsWindow -log = Logger().get_logger("LoggingModule", "logging") - class LoggingModule: def __init__(self, main_parent=None, parent=None): self.parent = parent + self.log = Logger().get_logger(self.__class__.__name__, "logging") - self.window = LogsWindow() + try: + self.window = LogsWindow() + self.tray_menu = self._tray_menu + except Exception: + self.log.warning( + "Couldn't set Logging GUI due to error.", exc_info=True + ) # Definition of Tray menu - def tray_menu(self, parent_menu): + def _tray_menu(self, parent_menu): # Menu for Tray App menu = QtWidgets.QMenu('Logging', parent_menu) # menu.setProperty('submenu', 'on') diff --git a/pype/modules/standalonepublish/publish.py b/pype/modules/standalonepublish/publish.py index 43d3b029dc..dd65030f7a 100644 --- a/pype/modules/standalonepublish/publish.py +++ b/pype/modules/standalonepublish/publish.py @@ -147,7 +147,7 @@ def cli_publish(data, gui=True): envcopy["PYBLISH_HOSTS"] = "standalonepublisher" envcopy["SAPUBLISH_INPATH"] = json_data_path envcopy["SAPUBLISH_OUTPATH"] = return_data_path - envcopy["PYBLISH_GUI"] = "pyblish_lite" + envcopy["PYBLISH_GUI"] = "pyblish_pype" returncode = execute([ sys.executable, "-u", "-m", "pyblish" diff --git a/pype/plugins/celaction/publish/collect_audio.py b/pype/plugins/celaction/publish/collect_audio.py new file mode 100644 index 0000000000..610b81d056 --- /dev/null +++ b/pype/plugins/celaction/publish/collect_audio.py @@ -0,0 +1,41 @@ +import pyblish.api +import os + +import pype.api as pype +from pprint import pformat + + +class AppendCelactionAudio(pyblish.api.ContextPlugin): + + label = "Colect Audio for publishing" + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, context): + self.log.info('Collecting Audio Data') + asset_entity = context.data["assetEntity"] + + # get all available representations + subsets = pype.get_subsets(asset_entity["name"], + representations=["audio"] + ) + self.log.info(f"subsets is: {pformat(subsets)}") + + if not subsets.get("audioMain"): + raise AttributeError("`audioMain` subset does not exist") + + reprs = subsets.get("audioMain", {}).get("representations", []) + self.log.info(f"reprs is: {pformat(reprs)}") + + repr = next((r for r in reprs), None) + if not repr: + raise "Missing `audioMain` representation" + self.log.info(f"represetation is: {repr}") + + audio_file = repr.get('data', {}).get('path', "") + + if os.path.exists(audio_file): + context.data["audioFile"] = audio_file + self.log.info( + 'audio_file: {}, has been added to context'.format(audio_file)) + else: + self.log.warning("Couldn't find any audio file on Ftrack.") diff --git a/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py b/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py new file mode 100644 index 0000000000..5042a7b700 --- /dev/null +++ b/pype/plugins/celaction/publish/collect_celaction_cli_kwargs.py @@ -0,0 +1,23 @@ +import pyblish.api +import pype.celaction + + +class CollectCelactionCliKwargs(pyblish.api.Collector): + """ Collects all keyword arguments passed from the terminal """ + + label = "Collect Celaction Cli Kwargs" + order = pyblish.api.Collector.order - 0.1 + + def process(self, context): + kwargs = pype.celaction.kwargs.copy() + + self.log.info("Storing kwargs: %s" % kwargs) + context.set_data("kwargs", kwargs) + + # get kwargs onto context data as keys with values + for k, v in kwargs.items(): + self.log.info(f"Setting `{k}` to instance.data with value: `{v}`") + if k in ["frameStart", "frameEnd"]: + context.data[k] = kwargs[k] = int(v) + else: + context.data[k] = v diff --git a/pype/plugins/celaction/publish/collect_celaction_instances.py b/pype/plugins/celaction/publish/collect_celaction_instances.py new file mode 100644 index 0000000000..aa2bb5da5d --- /dev/null +++ b/pype/plugins/celaction/publish/collect_celaction_instances.py @@ -0,0 +1,91 @@ +import os +from avalon import api +import pyblish.api + + +class CollectCelactionInstances(pyblish.api.ContextPlugin): + """ Adds the celaction render instances """ + + label = "Collect Celaction Instances" + order = pyblish.api.CollectorOrder + 0.1 + + def process(self, context): + task = api.Session["AVALON_TASK"] + current_file = context.data["currentFile"] + staging_dir = os.path.dirname(current_file) + scene_file = os.path.basename(current_file) + version = context.data["version"] + asset_entity = context.data["assetEntity"] + + shared_instance_data = { + "asset": asset_entity["name"], + "frameStart": asset_entity["data"]["frameStart"], + "frameEnd": asset_entity["data"]["frameEnd"], + "handleStart": asset_entity["data"]["handleStart"], + "handleEnd": asset_entity["data"]["handleEnd"], + "fps": asset_entity["data"]["fps"], + "resolutionWidth": asset_entity["data"]["resolutionWidth"], + "resolutionHeight": asset_entity["data"]["resolutionHeight"], + "pixelAspect": 1, + "step": 1, + "version": version + } + + celaction_kwargs = context.data.get("kwargs", {}) + + if celaction_kwargs: + shared_instance_data.update(celaction_kwargs) + + # workfile instance + family = "workfile" + subset = family + task.capitalize() + # Create instance + instance = context.create_instance(subset) + + # creating instance data + instance.data.update({ + "subset": subset, + "label": scene_file, + "family": family, + "families": [family], + "representations": list() + }) + + # adding basic script data + instance.data.update(shared_instance_data) + + # creating representation + representation = { + 'name': 'scn', + 'ext': 'scn', + 'files': scene_file, + "stagingDir": staging_dir, + } + + instance.data["representations"].append(representation) + + self.log.info('Publishing Celaction workfile') + + # render instance + family = "render.farm" + subset = f"render{task}Main" + instance = context.create_instance(name=subset) + # getting instance state + instance.data["publish"] = True + + # add assetEntity data into instance + instance.data.update({ + "label": "{} - farm".format(subset), + "family": family, + "families": [family], + "subset": subset + }) + + # adding basic script data + instance.data.update(shared_instance_data) + + self.log.info('Publishing Celaction render instance') + self.log.debug(f"Instance data: `{instance.data}`") + + for i in context: + self.log.debug(f"{i.data['families']}") diff --git a/pype/plugins/celaction/publish/collect_render_path.py b/pype/plugins/celaction/publish/collect_render_path.py new file mode 100644 index 0000000000..cddd2643d8 --- /dev/null +++ b/pype/plugins/celaction/publish/collect_render_path.py @@ -0,0 +1,29 @@ +import os +import pyblish.api + + +class CollectRenderPath(pyblish.api.InstancePlugin): + """Generate file and directory path where rendered images will be""" + + label = "Collect Render Path" + order = pyblish.api.CollectorOrder + 0.495 + + def process(self, instance): + anatomy = instance.context.data["anatomy"] + current_file = instance.context.data["currentFile"] + work_dir = os.path.dirname(current_file) + padding = anatomy.templates.get("frame_padding", 4) + render_dir = os.path.join( + work_dir, "render", "celaction" + ) + render_path = os.path.join( + render_dir, + ".".join([instance.data["subset"], f"%0{padding}d", "png"]) + ) + + # create dir if it doesnt exists + os.makedirs(render_dir, exist_ok=True) + + instance.data["path"] = render_path + + self.log.info(f"Render output path set to: `{render_path}`") diff --git a/pype/plugins/celaction/publish/integrate_version_up.py b/pype/plugins/celaction/publish/integrate_version_up.py new file mode 100644 index 0000000000..7fb1efa8aa --- /dev/null +++ b/pype/plugins/celaction/publish/integrate_version_up.py @@ -0,0 +1,68 @@ +import shutil +import re +import pyblish.api + + +class VersionUpScene(pyblish.api.ContextPlugin): + order = pyblish.api.IntegratorOrder + label = 'Version Up Scene' + families = ['scene'] + optional = True + active = True + + def process(self, context): + current_file = context.data.get('currentFile') + v_up = get_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) diff --git a/pype/plugins/celaction/publish/submit_celaction_deadline.py b/pype/plugins/celaction/publish/submit_celaction_deadline.py new file mode 100644 index 0000000000..0bb346f7cf --- /dev/null +++ b/pype/plugins/celaction/publish/submit_celaction_deadline.py @@ -0,0 +1,234 @@ +import os +import json +import getpass + +from avalon.vendor import requests +import re +import pyblish.api + + +class ExtractCelactionDeadline(pyblish.api.InstancePlugin): + """Submit CelAction2D scene to Deadline + + Renders are submitted to a Deadline Web Service as + supplied via the environment variable DEADLINE_REST_URL + + """ + + label = "Submit CelAction to Deadline" + order = pyblish.api.IntegratorOrder + 0.1 + hosts = ["celaction"] + families = ["render.farm"] + + deadline_department = "" + deadline_priority = 50 + deadline_pool = "" + deadline_pool_secondary = "" + deadline_group = "" + deadline_chunk_size = 1 + + def process(self, instance): + context = instance.context + + DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL") + assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" + + self.deadline_url = "{}/api/jobs".format(DEADLINE_REST_URL) + self._comment = context.data.get("comment", "") + self._deadline_user = context.data.get( + "deadlineUser", getpass.getuser()) + self._frame_start = int(instance.data["frameStart"]) + self._frame_end = int(instance.data["frameEnd"]) + + # get output path + render_path = instance.data['path'] + script_path = context.data["currentFile"] + + response = self.payload_submit(instance, + script_path, + render_path + ) + # Store output dir for unified publisher (filesequence) + instance.data["deadlineSubmissionJob"] = response.json() + + instance.data["outputDir"] = os.path.dirname( + render_path).replace("\\", "/") + + instance.data["publishJobState"] = "Suspended" + instance.context.data['ftrackStatus'] = "Render" + + # adding 2d render specific family for version identification in Loader + instance.data["families"] = ["render2d"] + + def payload_submit(self, + instance, + script_path, + render_path + ): + resolution_width = instance.data["resolutionWidth"] + resolution_height = instance.data["resolutionHeight"] + render_dir = os.path.normpath(os.path.dirname(render_path)) + script_name = os.path.basename(script_path) + jobname = "%s - %s" % (script_name, instance.name) + + output_filename_0 = self.preview_fname(render_path) + + try: + # Ensure render folder exists + os.makedirs(render_dir) + except OSError: + pass + + # define chunk and priority + chunk_size = instance.context.data.get("chunk") + if chunk_size == 0: + chunk_size = self.deadline_chunk_size + + # search for %02d pattern in name, and padding number + search_results = re.search(r"(.%0)(\d)(d)[._]", render_path).groups() + split_patern = "".join(search_results) + padding_number = int(search_results[1]) + + args = [ + f"{script_path}", + "-a", + "-s ", + "-e ", + f"-d {render_dir}", + f"-x {resolution_width}", + f"-y {resolution_height}", + f"-r {render_path.replace(split_patern, '')}", + f"-= AbsoluteFrameNumber=on -= PadDigits={padding_number}", + "-= ClearAttachment=on", + ] + + payload = { + "JobInfo": { + # Job name, as seen in Monitor + "Name": jobname, + + # plugin definition + "Plugin": "CelAction", + + # Top-level group name + "BatchName": script_name, + + # Arbitrary username, for visualisation in Monitor + "UserName": self._deadline_user, + + "Department": self.deadline_department, + "Priority": self.deadline_priority, + + "Group": self.deadline_group, + "Pool": self.deadline_pool, + "SecondaryPool": self.deadline_pool_secondary, + "ChunkSize": chunk_size, + + "Frames": f"{self._frame_start}-{self._frame_end}", + "Comment": self._comment, + + # Optional, enable double-click to preview rendered + # frames from Deadline Monitor + "OutputFilename0": output_filename_0.replace("\\", "/") + + }, + "PluginInfo": { + # Input + "SceneFile": script_path, + + # Output directory + "OutputFilePath": render_dir.replace("\\", "/"), + + # Plugin attributes + "StartupDirectory": "", + "Arguments": " ".join(args), + + # Resolve relative references + "ProjectPath": script_path, + "AWSAssetFile0": render_path, + }, + + # Mandatory for Deadline, may be empty + "AuxFiles": [] + } + + plugin = payload["JobInfo"]["Plugin"] + self.log.info("using render plugin : {}".format(plugin)) + + self.log.info("Submitting..") + self.log.info(json.dumps(payload, indent=4, sort_keys=True)) + + # adding expectied files to instance.data + self.expected_files(instance, render_path) + self.log.debug("__ expectedFiles: `{}`".format( + instance.data["expectedFiles"])) + response = requests.post(self.deadline_url, json=payload) + + if not response.ok: + raise Exception(response.text) + + return response + + def preflight_check(self, instance): + """Ensure the startFrame, endFrame and byFrameStep are integers""" + + for key in ("frameStart", "frameEnd"): + value = instance.data[key] + + if int(value) == value: + continue + + self.log.warning( + "%f=%d was rounded off to nearest integer" + % (value, int(value)) + ) + + def preview_fname(self, path): + """Return output file path with #### for padding. + + Deadline requires the path to be formatted with # in place of numbers. + For example `/path/to/render.####.png` + + Args: + path (str): path to rendered images + + Returns: + str + + """ + self.log.debug("_ path: `{}`".format(path)) + if "%" in path: + search_results = re.search(r"[._](%0)(\d)(d)[._]", path).groups() + split_patern = "".join(search_results) + split_path = path.split(split_patern) + hashes = "#" * int(search_results[1]) + return "".join([split_path[0], hashes, split_path[-1]]) + if "#" in path: + self.log.debug("_ path: `{}`".format(path)) + return path + else: + return path + + def expected_files(self, + instance, + path): + """ Create expected files in instance data + """ + if not instance.data.get("expectedFiles"): + instance.data["expectedFiles"] = list() + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + instance.data["expectedFiles"].append(path) + return + + for i in range(self._frame_start, (self._frame_end + 1)): + instance.data["expectedFiles"].append( + os.path.join(dir, (file % i)).replace("\\", "/")) diff --git a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py index 11b569fd12..f5d7689678 100644 --- a/pype/plugins/ftrack/publish/integrate_ftrack_instances.py +++ b/pype/plugins/ftrack/publish/integrate_ftrack_instances.py @@ -44,10 +44,14 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): family = instance.data['family'].lower() - asset_type = '' - asset_type = instance.data.get( - "ftrackFamily", self.family_mapping[family] - ) + asset_type = instance.data.get("ftrackFamily") + if not asset_type and family in self.family_mapping: + asset_type = self.family_mapping[family] + + # Ignore this instance if neither "ftrackFamily" or a family mapping is + # found. + if not asset_type: + return componentList = [] ft_session = instance.context.data["ftrackSession"] diff --git a/pype/plugins/global/publish/collect_avalon_entities.py b/pype/plugins/global/publish/collect_avalon_entities.py index 51dd3d7b06..917172d40c 100644 --- a/pype/plugins/global/publish/collect_avalon_entities.py +++ b/pype/plugins/global/publish/collect_avalon_entities.py @@ -48,8 +48,18 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): data = asset_entity['data'] - context.data["frameStart"] = data.get("frameStart") - context.data["frameEnd"] = data.get("frameEnd") + frame_start = data.get("frameStart") + if frame_start is None: + frame_start = 1 + self.log.warning("Missing frame start. Defaulting to 1.") + + frame_end = data.get("frameEnd") + if frame_end is None: + frame_end = 2 + self.log.warning("Missing frame end. Defaulting to 2.") + + context.data["frameStart"] = frame_start + context.data["frameEnd"] = frame_end handles = data.get("handles") or 0 handle_start = data.get("handleStart") @@ -72,7 +82,7 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): context.data["handleStart"] = int(handle_start) context.data["handleEnd"] = int(handle_end) - frame_start_h = data.get("frameStart") - context.data["handleStart"] - frame_end_h = data.get("frameEnd") + context.data["handleEnd"] + frame_start_h = frame_start - context.data["handleStart"] + frame_end_h = frame_end + context.data["handleEnd"] context.data["frameStartHandle"] = frame_start_h context.data["frameEndHandle"] = frame_end_h diff --git a/pype/plugins/global/publish/collect_rendered_files.py b/pype/plugins/global/publish/collect_rendered_files.py index 5229cd9705..e0f3695fd5 100644 --- a/pype/plugins/global/publish/collect_rendered_files.py +++ b/pype/plugins/global/publish/collect_rendered_files.py @@ -99,6 +99,17 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): instance.data["representations"] = representations + # add audio if in metadata data + if data.get("audio"): + instance.data.update({ + "audio": [{ + "filename": data.get("audio"), + "offset": 0 + }] + }) + self.log.info( + f"Adding audio to instance: {instance.data['audio']}") + def process(self, context): self._context = context diff --git a/pype/plugins/global/publish/extract_hierarchy_avalon.py b/pype/plugins/global/publish/extract_hierarchy_avalon.py index ab8226f6ef..83cf03b042 100644 --- a/pype/plugins/global/publish/extract_hierarchy_avalon.py +++ b/pype/plugins/global/publish/extract_hierarchy_avalon.py @@ -7,7 +7,7 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.01 label = "Extract Hierarchy To Avalon" - families = ["clip", "shot"] + families = ["clip", "shot", "editorial"] def process(self, context): if "hierarchyContext" not in context.data: diff --git a/pype/plugins/global/publish/extract_review.py b/pype/plugins/global/publish/extract_review.py index 0690d5cf80..0f15295118 100644 --- a/pype/plugins/global/publish/extract_review.py +++ b/pype/plugins/global/publish/extract_review.py @@ -610,8 +610,8 @@ class ExtractReview(pyblish.api.InstancePlugin): # NOTE Skipped using instance's resolution full_input_path_single_file = temp_data["full_input_path_single_file"] input_data = pype.lib.ffprobe_streams(full_input_path_single_file)[0] - input_width = input_data["width"] - input_height = input_data["height"] + input_width = int(input_data["width"]) + input_height = int(input_data["height"]) self.log.debug("pixel_aspect: `{}`".format(pixel_aspect)) self.log.debug("input_width: `{}`".format(input_width)) @@ -631,6 +631,9 @@ class ExtractReview(pyblish.api.InstancePlugin): output_width = input_width output_height = input_height + output_width = int(output_width) + output_height = int(output_height) + self.log.debug( "Output resolution is {}x{}".format(output_width, output_height) ) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index f8429e8b58..040ed9cd67 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -44,7 +44,6 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "frameStart" "frameEnd" 'fps' - "data": additional metadata for each representation. """ label = "Integrate Asset New" @@ -83,7 +82,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "fbx", "textures", "action", - "harmony.template" + "harmony.template", + "editorial" ] exclude_families = ["clip"] db_representation_context_keys = [ @@ -379,8 +379,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): dst = "{0}{1}{2}".format( dst_head, dst_padding, - dst_tail - ).replace("..", ".") + dst_tail).replace("..", ".") self.log.debug("destination: `{}`".format(dst)) src = os.path.join(stagingdir, src_file_name) @@ -453,15 +452,13 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): if repre_id is None: repre_id = io.ObjectId() - data = repre.get("data") or {} - data.update({'path': dst, 'template': template}) representation = { "_id": repre_id, "schema": "pype:representation-2.0", "type": "representation", "parent": version_id, "name": repre['name'], - "data": data, + "data": {'path': dst, 'template': template}, "dependencies": instance.data.get("dependencies", "").split(), # Imprint shortcut to context @@ -561,10 +558,17 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): while True: try: copyfile(src, dst) - except OSError as e: - self.log.critical("Cannot copy {} to {}".format(src, dst)) - self.log.critical(e) - six.reraise(*sys.exc_info()) + except (OSError, AttributeError) as e: + self.log.warning(e) + # try it again with shutil + import shutil + try: + shutil.copyfile(src, dst) + self.log.debug("Copying files with shutil...") + except (OSError) as e: + self.log.critical("Cannot copy {} to {}".format(src, dst)) + self.log.critical(e) + six.reraise(*sys.exc_info()) if str(getsize(src)) in str(getsize(dst)): break diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 7a5657044b..82de2ec099 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -14,6 +14,11 @@ import pyblish.api def _get_script(): """Get path to the image sequence script.""" + try: + from pathlib import Path + except ImportError: + from pathlib2 import Path + try: from pype.scripts import publish_filesequence except Exception: @@ -23,7 +28,10 @@ def _get_script(): if module_path.endswith(".pyc"): module_path = module_path[: -len(".pyc")] + ".py" - return os.path.normpath(module_path) + path = Path(os.path.normpath(module_path)).resolve(strict=True) + assert path is not None, ("Cannot determine path") + + return str(path) def get_latest_version(asset_name, subset_name, family): @@ -145,7 +153,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.2 icon = "tractor" - hosts = ["fusion", "maya", "nuke"] + hosts = ["fusion", "maya", "nuke", "celaction"] families = ["render.farm", "prerener", "renderlayer", "imagesequence", "vrayscene"] @@ -158,11 +166,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "FTRACK_SERVER", "PYPE_METADATA_FILE", "AVALON_PROJECT", - "PYPE_LOG_NO_COLORS" + "PYPE_LOG_NO_COLORS", + "PYPE_PYTHON_EXE" ] - # pool used to do the publishing job + # custom deadline atributes + deadline_department = "" deadline_pool = "" + deadline_pool_secondary = "" + deadline_group = "" + deadline_chunk_size = 1 # regex for finding frame number in string R_FRAME_NUMBER = re.compile(r'.+\.(?P[0-9]+)\..+') @@ -215,8 +228,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "JobDependency0": job["_id"], "UserName": job["Props"]["User"], "Comment": instance.context.data.get("comment", ""), + + "Department": self.deadline_department, + "ChunkSize": self.deadline_chunk_size, "Priority": job["Props"]["Pri"], + + "Group": self.deadline_group, "Pool": self.deadline_pool, + "SecondaryPool": self.deadline_pool_secondary, + "OutputDirectory0": output_dir }, "PluginInfo": { @@ -470,6 +490,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if bake_render_path: preview = False + if "celaction" in self.hosts: + preview = True + staging = os.path.dirname(list(collection)[0]) success, rootless_staging_dir = ( self.anatomy.find_root_template_from_path(staging) @@ -819,6 +842,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "instances": instances } + # add audio to metadata file if available + audio_file = context.data.get("audioFile") + if audio_file and os.path.isfile(audio_file): + publish_job.update({"audio": audio_file}) + # pass Ftrack credentials in case of Muster if submission_type == "muster": ftrack = { diff --git a/pype/plugins/global/publish/validate_custom_ftrack_attributes.py b/pype/plugins/global/publish/validate_custom_ftrack_attributes.py index 1e8b239b33..633deaa6d1 100644 --- a/pype/plugins/global/publish/validate_custom_ftrack_attributes.py +++ b/pype/plugins/global/publish/validate_custom_ftrack_attributes.py @@ -37,6 +37,21 @@ class ValidateFtrackAttributes(pyblish.api.InstancePlugin): order = pype.api.ValidateContentsOrder families = ["ftrack"] optional = True + # Ignore standalone host, because it does not have an Ftrack entity + # associated. + hosts = [ + "blender", + "fusion", + "harmony", + "houdini", + "maya", + "nuke", + "nukestudio", + "photoshop", + "premiere", + "resolve", + "unreal" + ] def process(self, instance): context = instance.context diff --git a/pype/plugins/global/publish/validate_ffmpeg_installed.py b/pype/plugins/global/publish/validate_ffmpeg_installed.py index f6738e6de1..61127ff96e 100644 --- a/pype/plugins/global/publish/validate_ffmpeg_installed.py +++ b/pype/plugins/global/publish/validate_ffmpeg_installed.py @@ -8,12 +8,11 @@ except ImportError: import errno -class ValidateFFmpegInstalled(pyblish.api.Validator): +class ValidateFFmpegInstalled(pyblish.api.ContextPlugin): """Validate availability of ffmpeg tool in PATH""" order = pyblish.api.ValidatorOrder label = 'Validate ffmpeg installation' - families = ['review'] optional = True def is_tool(self, name): @@ -27,7 +26,7 @@ class ValidateFFmpegInstalled(pyblish.api.Validator): return False return True - def process(self, instance): + def process(self, context): ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") self.log.info("ffmpeg path: `{}`".format(ffmpeg_path)) if self.is_tool(ffmpeg_path) is False: diff --git a/pype/plugins/harmony/load/load_audio.py b/pype/plugins/harmony/load/load_audio.py new file mode 100644 index 0000000000..a17af78964 --- /dev/null +++ b/pype/plugins/harmony/load/load_audio.py @@ -0,0 +1,42 @@ +from avalon import api, harmony + + +func = """ +function getUniqueColumnName( column_prefix ) +{ + var suffix = 0; + // finds if unique name for a column + var column_name = column_prefix; + while(suffix < 2000) + { + if(!column.type(column_name)) + break; + + suffix = suffix + 1; + column_name = column_prefix + "_" + suffix; + } + return column_name; +} + +function func(args) +{ + var uniqueColumnName = getUniqueColumnName(args[0]); + column.add(uniqueColumnName , "SOUND"); + column.importSound(uniqueColumnName, 1, args[1]); +} +func +""" + + +class ImportAudioLoader(api.Loader): + """Import audio.""" + + families = ["shot"] + representations = ["wav"] + label = "Import Audio" + + def load(self, context, name=None, namespace=None, data=None): + wav_file = api.get_representation_path(context["representation"]) + harmony.send( + {"function": func, "args": [context["subset"]["name"], wav_file]} + ) diff --git a/pype/plugins/harmony/load/load_imagesequence.py b/pype/plugins/harmony/load/load_imagesequence.py new file mode 100644 index 0000000000..7862e027af --- /dev/null +++ b/pype/plugins/harmony/load/load_imagesequence.py @@ -0,0 +1,228 @@ +import os + +import clique + +from avalon import api, harmony + +copy_files = """function copyFile(srcFilename, dstFilename) +{ + var srcFile = new PermanentFile(srcFilename); + var dstFile = new PermanentFile(dstFilename); + srcFile.copy(dstFile); +} +""" + +import_files = """var PNGTransparencyMode = 0; //Premultiplied wih Black +var TGATransparencyMode = 0; //Premultiplied wih Black +var SGITransparencyMode = 0; //Premultiplied wih Black +var LayeredPSDTransparencyMode = 1; //Straight +var FlatPSDTransparencyMode = 2; //Premultiplied wih White + +function getUniqueColumnName( column_prefix ) +{ + var suffix = 0; + // finds if unique name for a column + var column_name = column_prefix; + while(suffix < 2000) + { + if(!column.type(column_name)) + break; + + suffix = suffix + 1; + column_name = column_prefix + "_" + suffix; + } + return column_name; +} + +function import_files(args) +{ + var root = args[0]; + var files = args[1]; + var name = args[2]; + var start_frame = args[3]; + + var vectorFormat = null; + var extension = null; + var filename = files[0]; + + var pos = filename.lastIndexOf("."); + if( pos < 0 ) + return null; + + extension = filename.substr(pos+1).toLowerCase(); + + if(extension == "jpeg") + extension = "jpg"; + if(extension == "tvg") + { + vectorFormat = "TVG" + extension ="SCAN"; // element.add() will use this. + } + + var elemId = element.add( + name, + "BW", + scene.numberOfUnitsZ(), + extension.toUpperCase(), + vectorFormat + ); + if (elemId == -1) + { + // hum, unknown file type most likely -- let's skip it. + return null; // no read to add. + } + + var uniqueColumnName = getUniqueColumnName(name); + column.add(uniqueColumnName , "DRAWING"); + column.setElementIdOfDrawing(uniqueColumnName, elemId); + + var read = node.add(root, name, "READ", 0, 0, 0); + var transparencyAttr = node.getAttr( + read, frame.current(), "READ_TRANSPARENCY" + ); + var opacityAttr = node.getAttr(read, frame.current(), "OPACITY"); + transparencyAttr.setValue(true); + opacityAttr.setValue(true); + + var alignmentAttr = node.getAttr(read, frame.current(), "ALIGNMENT_RULE"); + alignmentAttr.setValue("ASIS"); + + var transparencyModeAttr = node.getAttr( + read, frame.current(), "applyMatteToColor" + ); + if (extension == "png") + transparencyModeAttr.setValue(PNGTransparencyMode); + if (extension == "tga") + transparencyModeAttr.setValue(TGATransparencyMode); + if (extension == "sgi") + transparencyModeAttr.setValue(SGITransparencyMode); + if (extension == "psd") + transparencyModeAttr.setValue(FlatPSDTransparencyMode); + + node.linkAttr(read, "DRAWING.ELEMENT", uniqueColumnName); + + // Create a drawing for each file. + for( var i =0; i <= files.length - 1; ++i) + { + timing = start_frame + i + // Create a drawing drawing, 'true' indicate that the file exists. + Drawing.create(elemId, timing, true); + // Get the actual path, in tmp folder. + var drawingFilePath = Drawing.filename(elemId, timing.toString()); + copyFile( files[i], drawingFilePath ); + + column.setEntry(uniqueColumnName, 1, timing, timing.toString()); + } + return read; +} +import_files +""" + +replace_files = """function replace_files(args) +{ + var files = args[0]; + var _node = args[1]; + var start_frame = args[2]; + + var _column = node.linkedColumn(_node, "DRAWING.ELEMENT"); + + // Delete existing drawings. + var timings = column.getDrawingTimings(_column); + for( var i =0; i <= timings.length - 1; ++i) + { + column.deleteDrawingAt(_column, parseInt(timings[i])); + } + + // Create new drawings. + for( var i =0; i <= files.length - 1; ++i) + { + timing = start_frame + i + // Create a drawing drawing, 'true' indicate that the file exists. + Drawing.create(node.getElementId(_node), timing, true); + // Get the actual path, in tmp folder. + var drawingFilePath = Drawing.filename( + node.getElementId(_node), timing.toString() + ); + copyFile( files[i], drawingFilePath ); + + column.setEntry(_column, 1, timing, timing.toString()); + } +} +replace_files +""" + + +class ImageSequenceLoader(api.Loader): + """Load images + Stores the imported asset in a container named after the asset. + """ + families = ["shot", "render"] + representations = ["jpeg", "png"] + + def load(self, context, name=None, namespace=None, data=None): + + collections, remainder = clique.assemble( + os.listdir(os.path.dirname(self.fname)) + ) + files = [] + for f in list(collections[0]): + files.append( + os.path.join(os.path.dirname(self.fname), f).replace("\\", "/") + ) + + read_node = harmony.send( + { + "function": copy_files + import_files, + "args": ["Top", files, context["subset"]["name"], 1] + } + )["result"] + + self[:] = [read_node] + + return harmony.containerise( + name, + namespace, + read_node, + context, + self.__class__.__name__ + ) + + def update(self, container, representation): + node = container.pop("node") + + collections, remainder = clique.assemble( + os.listdir( + os.path.dirname(api.get_representation_path(representation)) + ) + ) + files = [] + for f in list(collections[0]): + files.append( + os.path.join(os.path.dirname(self.fname), f).replace("\\", "/") + ) + + harmony.send( + { + "function": copy_files + replace_files, + "args": [files, node, 1] + } + ) + + harmony.imprint( + node, {"representation": str(representation["_id"])} + ) + + def remove(self, container): + node = container.pop("node") + func = """function deleteNode(_node) + { + node.deleteNode(_node, true, true); + } + deleteNode + """ + harmony.send( + {"function": func, "args": [node]} + ) + + def switch(self, container, representation): + self.update(container, representation) diff --git a/pype/plugins/harmony/load/load_template.py b/pype/plugins/harmony/load/load_template_workfile.py similarity index 56% rename from pype/plugins/harmony/load/load_template.py rename to pype/plugins/harmony/load/load_template_workfile.py index 53e0ff1f07..a9dcd0c776 100644 --- a/pype/plugins/harmony/load/load_template.py +++ b/pype/plugins/harmony/load/load_template_workfile.py @@ -14,18 +14,6 @@ class ImportTemplateLoader(api.Loader): label = "Import Template" def load(self, context, name=None, namespace=None, data=None): - # Make backdrops from metadata. - backdrops = context["representation"]["data"].get("backdrops", []) - - func = """function func(args) - { - Backdrop.addBackdrop("Top", args[0]); - } - func - """ - for backdrop in backdrops: - harmony.send({"function": func, "args": [backdrop]}) - # Import template. temp_dir = tempfile.mkdtemp() zip_file = api.get_representation_path(context["representation"]) @@ -33,19 +21,6 @@ class ImportTemplateLoader(api.Loader): with zipfile.ZipFile(zip_file, "r") as zip_ref: zip_ref.extractall(template_path) - func = """function func(args) - { - var template_path = args[0]; - var drag_object = copyPaste.copyFromTemplate( - template_path, 0, 0, copyPaste.getCurrentCreateOptions() - ); - copyPaste.pasteNewNodes( - drag_object, "", copyPaste.getCurrentPasteOptions() - ); - } - func - """ - func = """function func(args) { var template_path = args[0]; @@ -59,3 +34,11 @@ class ImportTemplateLoader(api.Loader): harmony.send({"function": func, "args": [template_path]}) shutil.rmtree(temp_dir) + + +class ImportWorkfileLoader(ImportTemplateLoader): + """Import workfiles.""" + + families = ["workfile"] + representations = ["*"] + label = "Import Workfile" diff --git a/pype/plugins/harmony/publish/collect_workfile.py b/pype/plugins/harmony/publish/collect_workfile.py new file mode 100644 index 0000000000..7781eb0774 --- /dev/null +++ b/pype/plugins/harmony/publish/collect_workfile.py @@ -0,0 +1,28 @@ +import pyblish.api +import os + + +class CollectWorkfile(pyblish.api.ContextPlugin): + """Collect current script for publish.""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect Workfile" + hosts = ["harmony"] + + def process(self, context): + family = "workfile" + task = os.getenv("AVALON_TASK", None) + subset = family + task.capitalize() + basename = os.path.basename(context.data["currentFile"]) + + # Create instance + instance = context.create_instance(subset) + instance.data.update({ + "subset": subset, + "label": basename, + "name": basename, + "family": family, + "families": [], + "representations": [], + "asset": os.environ["AVALON_ASSET"] + }) diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index de6e8b9008..7ca83d3f0f 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -1,9 +1,9 @@ import os import tempfile +import subprocess import pyblish.api from avalon import harmony -import pype.lib import clique @@ -28,7 +28,8 @@ class ExtractRender(pyblish.api.InstancePlugin): scene.currentScene(), scene.getFrameRate(), scene.getStartFrame(), - scene.getStopFrame() + scene.getStopFrame(), + sound.getSoundtrackAll().path() ] } func @@ -37,11 +38,11 @@ class ExtractRender(pyblish.api.InstancePlugin): {"function": func, "args": [instance[0]]} )["result"] application_path = result[0] - project_path = result[1] scene_path = os.path.join(result[1], result[2] + ".xstage") frame_rate = result[3] frame_start = result[4] frame_end = result[5] + audio_path = result[6] # Set output path to temp folder. path = tempfile.mkdtemp() @@ -59,9 +60,16 @@ class ExtractRender(pyblish.api.InstancePlugin): ) harmony.save_scene() - # Execute rendering. - output = pype.lib._subprocess([application_path, "-batch", scene_path]) - self.log.info(output) + # Execute rendering. Ignoring error cause Harmony returns error code + # always. + proc = subprocess.Popen( + [application_path, "-batch", scene_path], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE + ) + output, error = proc.communicate() + self.log.info(output.decode("utf-8")) # Collect rendered files. files = os.listdir(path) @@ -76,25 +84,85 @@ class ExtractRender(pyblish.api.InstancePlugin): path, len(collections) ) ) + collection = collections[0] - extension = os.path.splitext(list(collections[0])[0])[-1][1:] + # Generate thumbnail. + thumbnail_path = os.path.join(path, "thumbnail.png") + args = [ + "ffmpeg", "-y", + "-i", os.path.join(path, list(collections[0])[0]), + "-vf", "scale=300:-1", + "-vframes", "1", + thumbnail_path + ] + process = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE + ) + + output = process.communicate()[0] + + if process.returncode != 0: + raise ValueError(output.decode("utf-8")) + + self.log.debug(output.decode("utf-8")) + + # Generate mov. + mov_path = os.path.join(path, instance.data["name"] + ".mov") + args = [ + "ffmpeg", "-y", + "-i", audio_path, + "-i", + os.path.join(path, collection.head + "%04d" + collection.tail), + mov_path + ] + process = subprocess.Popen( + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE + ) + + output = process.communicate()[0] + + if process.returncode != 0: + raise ValueError(output.decode("utf-8")) + + self.log.debug(output.decode("utf-8")) + + # Generate representations. + extension = collection.tail[1:] representation = { "name": extension, "ext": extension, - "files": list(collections[0]), + "files": list(collection), + "stagingDir": path + } + movie = { + "name": "mov", + "ext": "mov", + "files": os.path.basename(mov_path), "stagingDir": path, "frameStart": frame_start, "frameEnd": frame_end, "fps": frame_rate, "preview": True, - "tags": ["review"] + "tags": ["review", "ftrackreview"] } - instance.data["representations"] = [representation] - self.log.info(frame_rate) + thumbnail = { + "name": "thumbnail", + "ext": "png", + "files": os.path.basename(thumbnail_path), + "stagingDir": path, + "tags": ["thumbnail"] + } + instance.data["representations"] = [representation, movie, thumbnail] # Required for extract_review plugin (L222 onwards). instance.data["frameStart"] = frame_start instance.data["frameEnd"] = frame_end instance.data["fps"] = frame_rate - self.log.info("Extracted {instance} to {path}".format(**locals())) + self.log.info(f"Extracted {instance} to {path}") diff --git a/pype/plugins/harmony/publish/extract_save_scene.py b/pype/plugins/harmony/publish/extract_save_scene.py new file mode 100644 index 0000000000..8b953580a7 --- /dev/null +++ b/pype/plugins/harmony/publish/extract_save_scene.py @@ -0,0 +1,13 @@ +import pyblish.api +from avalon import harmony + + +class ExtractSaveScene(pyblish.api.ContextPlugin): + """Save scene for extraction.""" + + label = "Extract Save Scene" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["harmony"] + + def process(self, context): + harmony.save_scene() diff --git a/pype/plugins/harmony/publish/extract_template.py b/pype/plugins/harmony/publish/extract_template.py index f7a5e34e67..1ba0befc54 100644 --- a/pype/plugins/harmony/publish/extract_template.py +++ b/pype/plugins/harmony/publish/extract_template.py @@ -2,7 +2,8 @@ import os import shutil import pype.api -from avalon import harmony +import avalon.harmony +import pype.hosts.harmony class ExtractTemplate(pype.api.Extractor): @@ -14,6 +15,7 @@ class ExtractTemplate(pype.api.Extractor): def process(self, instance): staging_dir = self.staging_dir(instance) + filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name)) self.log.info("Outputting template to {}".format(staging_dir)) @@ -28,7 +30,7 @@ class ExtractTemplate(pype.api.Extractor): unique_backdrops = [backdrops[x] for x in set(backdrops.keys())] # Get non-connected nodes within backdrops. - all_nodes = harmony.send( + all_nodes = avalon.harmony.send( {"function": "node.subNodes", "args": ["Top"]} )["result"] for node in [x for x in all_nodes if x not in dependencies]: @@ -43,48 +45,9 @@ class ExtractTemplate(pype.api.Extractor): dependencies.remove(instance[0]) # Export template. - func = """function func(args) - { - // 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(); - selection.addNodeToSelection(template_group); - Action.perform("onActionEnterGroup()", "Node View"); - - // Recreate backdrops in group. - for (var i = 0 ; i < args[0].length; 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]); - - // Unfocus the group in Node view, delete all nodes and backdrops - // created during the process. - Action.perform("onActionUpToParent()", "Node View"); - node.deleteNode(template_group, true, true); - } - func - """ - harmony.send({ - "function": func, - "args": [ - unique_backdrops, - dependencies, - "{}.tpl".format(instance.name), - staging_dir - ] - }) + pype.hosts.harmony.export_template( + unique_backdrops, dependencies, filepath + ) # Prep representation. os.chdir(staging_dir) @@ -131,7 +94,7 @@ class ExtractTemplate(pype.api.Extractor): } func """ - return harmony.send( + return avalon.harmony.send( {"function": func, "args": [node]} )["result"] @@ -150,7 +113,7 @@ class ExtractTemplate(pype.api.Extractor): func """ - current_dependencies = harmony.send( + current_dependencies = avalon.harmony.send( {"function": func, "args": [node]} )["result"] diff --git a/pype/plugins/harmony/publish/extract_workfile.py b/pype/plugins/harmony/publish/extract_workfile.py new file mode 100644 index 0000000000..304b70e293 --- /dev/null +++ b/pype/plugins/harmony/publish/extract_workfile.py @@ -0,0 +1,43 @@ +import os +import shutil + +import pype.api +import avalon.harmony +import pype.hosts.harmony + + +class ExtractWorkfile(pype.api.Extractor): + """Extract the connected nodes to the composite instance.""" + + label = "Extract Workfile" + hosts = ["harmony"] + families = ["workfile"] + + def process(self, instance): + # Export template. + backdrops = avalon.harmony.send( + {"function": "Backdrop.backdrops", "args": ["Top"]} + )["result"] + nodes = avalon.harmony.send( + {"function": "node.subNodes", "args": ["Top"]} + )["result"] + staging_dir = self.staging_dir(instance) + filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name)) + + pype.hosts.harmony.export_template(backdrops, nodes, filepath) + + # Prep representation. + os.chdir(staging_dir) + shutil.make_archive( + "{}".format(instance.name), + "zip", + os.path.join(staging_dir, "{}.tpl".format(instance.name)) + ) + + representation = { + "name": "tpl", + "ext": "zip", + "files": "{}.zip".format(instance.name), + "stagingDir": staging_dir + } + instance.data["representations"] = [representation] diff --git a/pype/plugins/harmony/publish/increment_workfile.py b/pype/plugins/harmony/publish/increment_workfile.py new file mode 100644 index 0000000000..858e5fab0e --- /dev/null +++ b/pype/plugins/harmony/publish/increment_workfile.py @@ -0,0 +1,37 @@ +import os + +import pyblish.api +from pype.action import get_errored_plugins_from_data +from pype.lib import version_up +from avalon import harmony + + +class IncrementWorkfile(pyblish.api.InstancePlugin): + """Increment the current workfile. + + Saves the current scene with an increased version number. + """ + + label = "Increment Workfile" + order = pyblish.api.IntegratorOrder + 9.0 + hosts = ["harmony"] + families = ["workfile"] + optional = True + + def process(self, instance): + errored_plugins = get_errored_plugins_from_data(instance.context) + if errored_plugins: + raise RuntimeError( + "Skipping incrementing current file because publishing failed." + ) + + scene_dir = version_up( + os.path.dirname(instance.context.data["currentFile"]) + ) + scene_path = os.path.join( + scene_dir, os.path.basename(scene_dir) + ".xstage" + ) + + harmony.save_scene_as(scene_path) + + self.log.info("Incremented workfile to: {}".format(scene_path)) diff --git a/pype/plugins/harmony/publish/validate_instances.py b/pype/plugins/harmony/publish/validate_instances.py new file mode 100644 index 0000000000..f084baf790 --- /dev/null +++ b/pype/plugins/harmony/publish/validate_instances.py @@ -0,0 +1,48 @@ +import os + +import pyblish.api +import pype.api +from avalon import harmony + + +class ValidateInstanceRepair(pyblish.api.Action): + """Repair the instance.""" + + 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 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) + + for instance in instances: + data = harmony.read(instance[0]) + data["asset"] = os.environ["AVALON_ASSET"] + harmony.imprint(instance[0], data) + + +class ValidateInstance(pyblish.api.InstancePlugin): + """Validate the instance asset is the current asset.""" + + label = "Validate Instance" + hosts = ["harmony"] + actions = [ValidateInstanceRepair] + order = pype.api.ValidateContentsOrder + + def process(self, instance): + instance_asset = instance.data["asset"] + current_asset = os.environ["AVALON_ASSET"] + msg = ( + "Instance asset is not the same as current asset:" + f"\nInstance: {instance_asset}\nCurrent: {current_asset}" + ) + assert instance_asset == current_asset, msg diff --git a/pype/plugins/harmony/publish/validate_scene_settings.py b/pype/plugins/harmony/publish/validate_scene_settings.py new file mode 100644 index 0000000000..aa9a70bd85 --- /dev/null +++ b/pype/plugins/harmony/publish/validate_scene_settings.py @@ -0,0 +1,66 @@ +import json + +import pyblish.api + +import avalon.harmony +import pype.hosts.harmony + + +class ValidateSceneSettingsRepair(pyblish.api.Action): + """Repair the instance.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + pype.hosts.harmony.set_scene_settings( + pype.hosts.harmony.get_asset_settings() + ) + + +class ValidateSceneSettings(pyblish.api.InstancePlugin): + """Ensure the scene settings are in sync with database.""" + + order = pyblish.api.ValidatorOrder + label = "Validate Scene Settings" + families = ["workfile"] + hosts = ["harmony"] + actions = [ValidateSceneSettingsRepair] + + def process(self, instance): + expected_settings = pype.hosts.harmony.get_asset_settings() + + # Harmony is expected to start at 1. + frame_start = expected_settings["frameStart"] + frame_end = expected_settings["frameEnd"] + expected_settings["frameEnd"] = frame_end - frame_start + 1 + expected_settings["frameStart"] = 1 + + func = """function func() + { + return { + "fps": scene.getFrameRate(), + "frameStart": scene.getStartFrame(), + "frameEnd": scene.getStopFrame(), + "resolutionWidth": scene.defaultResolutionX(), + "resolutionHeight": scene.defaultResolutionY() + }; + } + func + """ + current_settings = avalon.harmony.send({"function": func})["result"] + + invalid_settings = [] + for key, value in expected_settings.items(): + if value != current_settings[key]: + invalid_settings.append({ + "name": key, + "expected": value, + "current": current_settings[key] + }) + + msg = "Found invalid settings:\n{}".format( + json.dumps(invalid_settings, sort_keys=True, indent=4) + ) + assert not invalid_settings, msg diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index dbcade9b77..3b2048d8f0 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -68,7 +68,7 @@ class CreateRender(avalon.maya.Creator): _image_prefixes = { 'mentalray': 'maya///_', - 'vray': '"maya///', + 'vray': 'maya///', 'arnold': 'maya///_', 'renderman': 'maya///_', 'redshift': 'maya///_' @@ -179,7 +179,7 @@ class CreateRender(avalon.maya.Creator): self.data["framesPerTask"] = 1 self.data["whitelist"] = False self.data["machineList"] = "" - self.data["useMayaBatch"] = True + self.data["useMayaBatch"] = False self.data["vrayScene"] = False # Disable for now as this feature is not working yet # self.data["assScene"] = False diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 445f646cb5..03b14f76bb 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -157,7 +157,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # in expectedFiles. If so, raise error as we cannot attach AOV # (considered to be subset on its own) to another subset if attach_to: - assert len(exp_files[0].keys()) == 1, ( + assert isinstance(exp_files, list), ( "attaching multiple AOVs or renderable cameras to " "subset is not supported" ) @@ -332,9 +332,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): options["extendFrames"] = extend_frames options["overrideExistingFrame"] = override_frames - maya_render_plugin = "MayaBatch" - if not attributes.get("useMayaBatch", True): - maya_render_plugin = "MayaCmd" + maya_render_plugin = "MayaPype" + if attributes.get("useMayaBatch", True): + maya_render_plugin = "MayaBatch" options["mayaRenderPlugin"] = maya_render_plugin diff --git a/pype/plugins/maya/publish/extract_animation.py b/pype/plugins/maya/publish/extract_animation.py index bd57eca0f8..f7058b34f1 100644 --- a/pype/plugins/maya/publish/extract_animation.py +++ b/pype/plugins/maya/publish/extract_animation.py @@ -37,7 +37,7 @@ class ExtractAnimation(pype.api.Extractor): # Collect the start and end including handles start = instance.data["frameStart"] end = instance.data["frameEnd"] - handles = instance.data.get("handles", 0) + handles = instance.data.get("handles", 0) or 0 if handles: start -= handles end += handles @@ -50,7 +50,7 @@ class ExtractAnimation(pype.api.Extractor): path = os.path.join(parent_dir, filename) options = { - "step": instance.data.get("step", 1.0), + "step": instance.data.get("step", 1.0) or 1.0, "attr": ["cbId"], "writeVisibility": True, "writeCreases": True, @@ -74,8 +74,8 @@ class ExtractAnimation(pype.api.Extractor): with avalon.maya.maintained_selection(): cmds.select(nodes, noExpand=True) extract_alembic(file=path, - startFrame=start, - endFrame=end, + startFrame=float(start), + endFrame=float(end), **options) if "representations" not in instance.data: diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 5a8b2f6e5a..8750d88b90 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -41,7 +41,7 @@ payload_skeleton = { "BatchName": None, # Top-level group name "Name": None, # Job name, as seen in Monitor "UserName": None, - "Plugin": "MayaBatch", + "Plugin": "MayaPype", "Frames": "{start}-{end}x{step}", "Comment": None, }, @@ -274,7 +274,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): step=int(self._instance.data["byFrameStep"])) payload_skeleton["JobInfo"]["Plugin"] = self._instance.data.get( - "mayaRenderPlugin", "MayaBatch") + "mayaRenderPlugin", "MayaPype") payload_skeleton["JobInfo"]["BatchName"] = filename # Job name, as seen in Monitor @@ -311,12 +311,14 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AVALON_TASK", "PYPE_USERNAME", "PYPE_DEV", - "PYPE_LOG_NO_COLORS" + "PYPE_LOG_NO_COLORS", + "PYPE_SETUP_PATH" ] environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) environment["PYPE_LOG_NO_COLORS"] = "1" + environment["PYPE_MAYA_VERSION"] = cmds.about(v=True) payload_skeleton["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, @@ -428,7 +430,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): int(self._instance.data["frameStartHandle"]), int(self._instance.data["frameEndHandle"])), - "Plugin": "MayaBatch", + "Plugin": self._instance.data.get( + "mayaRenderPlugin", "MayaPype"), "FramesPerTask": self._instance.data.get("framesPerTask", 1) } diff --git a/pype/plugins/maya/publish/validate_mesh_arnold_attributes.py b/pype/plugins/maya/publish/validate_mesh_arnold_attributes.py index 1ddf0285d4..04a3cf3c79 100644 --- a/pype/plugins/maya/publish/validate_mesh_arnold_attributes.py +++ b/pype/plugins/maya/publish/validate_mesh_arnold_attributes.py @@ -1,5 +1,5 @@ import pymel.core as pc - +from maya import cmds import pyblish.api import pype.api import pype.hosts.maya.action @@ -23,6 +23,11 @@ class ValidateMeshArnoldAttributes(pyblish.api.InstancePlugin): pype.api.RepairAction ] optional = True + if cmds.getAttr( + "defaultRenderGlobals.currentRenderer").lower() == "arnold": + active = True + else: + active = False @classmethod def get_invalid_attributes(cls, instance, compute=False): diff --git a/pype/plugins/maya/publish/validate_transform_naming_suffix.py b/pype/plugins/maya/publish/validate_transform_naming_suffix.py index bd7db437fa..17066f6b12 100644 --- a/pype/plugins/maya/publish/validate_transform_naming_suffix.py +++ b/pype/plugins/maya/publish/validate_transform_naming_suffix.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Plugin for validating naming conventions.""" from maya import cmds import pyblish.api @@ -42,7 +44,8 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): ALLOW_IF_NOT_IN_SUFFIX_TABLE = True @staticmethod - def is_valid_name(node_name, shape_type, SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): + def is_valid_name(node_name, shape_type, + SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): """Return whether node's name is correct. The correctness for a transform's suffix is dependent on what @@ -52,6 +55,12 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): When `shape_type` is None the transform doesn't have any direct children shapes. + Args: + node_name (str): Node name. + shape_type (str): Type of node. + SUFFIX_NAMING_TABLE (dict): Mapping dict for suffixes. + ALLOW_IF_NOT_IN_SUFFIX_TABLE (dict): Filter dict. + """ if shape_type not in SUFFIX_NAMING_TABLE: return ALLOW_IF_NOT_IN_SUFFIX_TABLE @@ -63,7 +72,13 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): return False @classmethod - def get_invalid(cls, instance, SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): + def get_invalid(cls, instance): + """Get invalid nodes in instance. + + Args: + instance (:class:`pyblish.api.Instance`): published instance. + + """ transforms = cmds.ls(instance, type='transform', long=True) invalid = [] @@ -74,16 +89,23 @@ class ValidateTransformNamingSuffix(pyblish.api.InstancePlugin): noIntermediate=True) shape_type = cmds.nodeType(shapes[0]) if shapes else None - if not cls.is_valid_name(transform, shape_type, SUFFIX_NAMING_TABLE, ALLOW_IF_NOT_IN_SUFFIX_TABLE): + if not cls.is_valid_name(transform, shape_type, + cls.SUFFIX_NAMING_TABLE, + cls.ALLOW_IF_NOT_IN_SUFFIX_TABLE): invalid.append(transform) return invalid def process(self, instance): - """Process all the nodes in the instance""" + """Process all the nodes in the instance. + Args: + instance (:class:`pyblish.api.Instance`): published instance. - invalid = self.get_invalid(instance, self.SUFFIX_NAMING_TABLE, self.ALLOW_IF_NOT_IN_SUFFIX_TABLE) + """ + invalid = self.get_invalid(instance, + self.SUFFIX_NAMING_TABLE, + self.ALLOW_IF_NOT_IN_SUFFIX_TABLE) if invalid: raise ValueError("Incorrectly named geometry " "transforms: {0}".format(invalid)) diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index a840dd13a7..ff0a5dcb6c 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -1,12 +1,77 @@ -from avalon import photoshop +from avalon import api, photoshop +from avalon.vendor import Qt -class CreateImage(photoshop.Creator): +class CreateImage(api.Creator): """Image folder for publish.""" name = "imageDefault" label = "Image" family = "image" - def __init__(self, *args, **kwargs): - super(CreateImage, self).__init__(*args, **kwargs) + def process(self): + groups = [] + layers = [] + create_group = False + group_constant = photoshop.get_com_objects().constants().psLayerSet + if (self.options or {}).get("useSelection"): + multiple_instances = False + selection = photoshop.get_selected_layers() + + if len(selection) > 1: + # Ask user whether to create one image or image per selected + # item. + msg_box = Qt.QtWidgets.QMessageBox() + msg_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg_box.setText( + "Multiple layers selected." + "\nDo you want to make one image per layer?" + ) + msg_box.setStandardButtons( + Qt.QtWidgets.QMessageBox.Yes | + Qt.QtWidgets.QMessageBox.No | + Qt.QtWidgets.QMessageBox.Cancel + ) + ret = msg_box.exec_() + if ret == Qt.QtWidgets.QMessageBox.Yes: + multiple_instances = True + elif ret == Qt.QtWidgets.QMessageBox.Cancel: + return + + if multiple_instances: + for item in selection: + if item.LayerType == group_constant: + groups.append(item) + else: + layers.append(item) + else: + group = photoshop.group_selected_layers() + group.Name = self.name + groups.append(group) + + elif len(selection) == 1: + # One selected item. Use group if its a LayerSet (group), else + # create a new group. + if selection[0].LayerType == group_constant: + groups.append(selection[0]) + else: + layers.append(selection[0]) + elif len(selection) == 0: + # No selection creates an empty group. + create_group = True + else: + create_group = True + + if create_group: + group = photoshop.app().ActiveDocument.LayerSets.Add() + group.Name = self.name + groups.append(group) + + for layer in layers: + photoshop.select_layers([layer]) + group = photoshop.group_selected_layers() + group.Name = layer.Name + groups.append(group) + + for group in groups: + photoshop.imprint(group, self.data) diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py new file mode 100644 index 0000000000..ba9ab8606a --- /dev/null +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -0,0 +1,29 @@ +import pyblish.api +from pype.action import get_errored_plugins_from_data +from pype.lib import version_up +from avalon import photoshop + + +class IncrementWorkfile(pyblish.api.InstancePlugin): + """Increment the current workfile. + + Saves the current scene with an increased version number. + """ + + label = "Increment Workfile" + order = pyblish.api.IntegratorOrder + 9.0 + hosts = ["photoshop"] + families = ["workfile"] + optional = True + + def process(self, instance): + errored_plugins = get_errored_plugins_from_data(instance.context) + if errored_plugins: + raise RuntimeError( + "Skipping incrementing current file because publishing failed." + ) + + scene_path = version_up(instance.context.data["currentFile"]) + photoshop.app().ActiveDocument.SaveAs(scene_path) + + self.log.info("Incremented workfile to: {}".format(scene_path)) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py new file mode 100644 index 0000000000..1d85ea99a0 --- /dev/null +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -0,0 +1,44 @@ +import pyblish.api +import pype.api + + +class ValidateNamingRepair(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 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) + + for instance in instances: + instance[0].Name = instance.data["name"].replace(" ", "_") + + return True + + +class ValidateNaming(pyblish.api.InstancePlugin): + """Validate the instance name. + + Spaces in names are not allowed. Will be replace with underscores. + """ + + label = "Validate Naming" + hosts = ["photoshop"] + order = pype.api.ValidateContentsOrder + families = ["image"] + actions = [ValidateNamingRepair] + + def process(self, instance): + msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) + assert " " not in instance.data["name"], msg diff --git a/pype/plugins/standalonepublisher/publish/collect_shots.py b/pype/plugins/standalonepublisher/publish/collect_shots.py new file mode 100644 index 0000000000..853ba4e8de --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/collect_shots.py @@ -0,0 +1,136 @@ +import os + +import opentimelineio as otio +from bson import json_util + +import pyblish.api +from pype import lib +from avalon import io + + +class OTIO_View(pyblish.api.Action): + """Currently disabled because OTIO requires PySide2. Issue on Qt.py: + https://github.com/PixarAnimationStudios/OpenTimelineIO/issues/289 + """ + + label = "OTIO View" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + instance = context[0] + representation = instance.data["representations"][0] + file_path = os.path.join( + representation["stagingDir"], representation["files"] + ) + lib._subprocess(["otioview", file_path]) + + +class CollectShots(pyblish.api.InstancePlugin): + """Collect Anatomy object into Context""" + + order = pyblish.api.CollectorOrder + label = "Collect Shots" + hosts = ["standalonepublisher"] + families = ["editorial"] + actions = [] + + def process(self, instance): + representation = instance.data["representations"][0] + file_path = os.path.join( + representation["stagingDir"], representation["files"] + ) + instance.context.data["editorialPath"] = file_path + + extension = os.path.splitext(file_path)[1][1:] + kwargs = {} + if extension == "edl": + # EDL has no frame rate embedded so needs explicit frame rate else + # 24 is asssumed. + kwargs["rate"] = lib.get_asset()["data"]["fps"] + + timeline = otio.adapters.read_from_file(file_path, **kwargs) + tracks = timeline.each_child( + descended_from_type=otio.schema.track.Track + ) + asset_entity = instance.context.data["assetEntity"] + asset_name = asset_entity["name"] + + # Project specific prefix naming. This needs to be replaced with some + # options to be more flexible. + asset_name = asset_name.split("_")[0] + + instances = [] + for track in tracks: + for child in track.each_child(): + + # Transitions are ignored, because Clips have the full frame + # range. + if isinstance(child, otio.schema.transition.Transition): + continue + + # Hardcoded to expect a shot name of "[name].[extension]" + child_name = os.path.splitext(child.name)[0].lower() + name = f"{asset_name}_{child_name}" + + frame_start = child.range_in_parent().start_time.value + frame_end = child.range_in_parent().end_time_inclusive().value + + label = f"{name} (framerange: {frame_start}-{frame_end})" + instances.append( + instance.context.create_instance(**{ + "name": name, + "label": label, + "frameStart": frame_start, + "frameEnd": frame_end, + "family": "shot", + "families": ["review", "ftrack"], + "ftrackFamily": "review", + "asset": name, + "subset": "shotMain", + "representations": [], + "source": file_path + }) + ) + + visual_hierarchy = [asset_entity] + while True: + visual_parent = io.find_one( + {"_id": visual_hierarchy[-1]["data"]["visualParent"]} + ) + if visual_parent: + visual_hierarchy.append(visual_parent) + else: + visual_hierarchy.append(instance.context.data["projectEntity"]) + break + + context_hierarchy = None + for entity in visual_hierarchy: + childs = {} + if context_hierarchy: + name = context_hierarchy.pop("name") + childs = {name: context_hierarchy} + else: + for instance in instances: + childs[instance.data["name"]] = { + "childs": {}, + "entity_type": "Shot", + "custom_attributes": { + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"] + } + } + + context_hierarchy = { + "entity_type": entity["data"]["entityType"], + "childs": childs, + "name": entity["name"] + } + + name = context_hierarchy.pop("name") + context_hierarchy = {name: context_hierarchy} + instance.context.data["hierarchyContext"] = context_hierarchy + self.log.info( + "Hierarchy:\n" + + json_util.dumps(context_hierarchy, sort_keys=True, indent=4) + ) diff --git a/pype/plugins/standalonepublisher/publish/extract_review.py b/pype/plugins/standalonepublisher/publish/extract_review.py index 36793d4c62..0f845afcb1 100644 --- a/pype/plugins/standalonepublisher/publish/extract_review.py +++ b/pype/plugins/standalonepublisher/publish/extract_review.py @@ -42,7 +42,7 @@ class ExtractReviewSP(pyblish.api.InstancePlugin): self.log.debug("Families In: `{}`".format(instance.data["families"])) # get specific profile if was defined - specific_profiles = instance.data.get("repreProfiles") + specific_profiles = instance.data.get("repreProfiles", []) new_repres = [] # filter out mov and img sequences diff --git a/pype/plugins/standalonepublisher/publish/extract_shot.py b/pype/plugins/standalonepublisher/publish/extract_shot.py new file mode 100644 index 0000000000..d58ddfe8d5 --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/extract_shot.py @@ -0,0 +1,96 @@ +import os + +import clique + +import pype.api +import pype.lib + + +class ExtractShot(pype.api.Extractor): + """Extract shot "mov" and "wav" files.""" + + label = "Extract Shot" + hosts = ["standalonepublisher"] + families = ["shot"] + + def process(self, instance): + staging_dir = self.staging_dir(instance) + self.log.info("Outputting shot to {}".format(staging_dir)) + + editorial_path = instance.context.data["editorialPath"] + basename = os.path.splitext(os.path.basename(editorial_path))[0] + + # Generate mov file. + fps = pype.lib.get_asset()["data"]["fps"] + input_path = os.path.join( + os.path.dirname(editorial_path), basename + ".mov" + ) + shot_mov = os.path.join(staging_dir, instance.data["name"] + ".mov") + ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") + args = [ + ffmpeg_path, + "-ss", str(instance.data["frameStart"] / fps), + "-i", input_path, + "-t", str( + (instance.data["frameEnd"] - instance.data["frameStart"] + 1) / + fps + ), + "-crf", "18", + "-pix_fmt", "yuv420p", + shot_mov + ] + self.log.info(f"Processing: {args}") + output = pype.lib._subprocess(args) + self.log.info(output) + + instance.data["representations"].append({ + "name": "mov", + "ext": "mov", + "files": os.path.basename(shot_mov), + "stagingDir": staging_dir, + "frameStart": instance.data["frameStart"], + "frameEnd": instance.data["frameEnd"], + "fps": fps, + "thumbnail": True, + "tags": ["review", "ftrackreview"] + }) + + # Generate jpegs. + shot_jpegs = os.path.join( + staging_dir, instance.data["name"] + ".%04d.jpeg" + ) + args = [ffmpeg_path, "-i", shot_mov, shot_jpegs] + self.log.info(f"Processing: {args}") + output = pype.lib._subprocess(args) + self.log.info(output) + + collection = clique.Collection( + head=instance.data["name"] + ".", tail='.jpeg', padding=4 + ) + for f in os.listdir(staging_dir): + if collection.match(f): + collection.add(f) + + instance.data["representations"].append({ + "name": "jpeg", + "ext": "jpeg", + "files": list(collection), + "stagingDir": staging_dir + }) + + # Generate wav file. + shot_wav = os.path.join(staging_dir, instance.data["name"] + ".wav") + args = [ffmpeg_path, "-i", shot_mov, shot_wav] + self.log.info(f"Processing: {args}") + output = pype.lib._subprocess(args) + self.log.info(output) + + instance.data["representations"].append({ + "name": "wav", + "ext": "wav", + "files": os.path.basename(shot_wav), + "stagingDir": staging_dir + }) + + # Required for extract_review plugin (L222 onwards). + instance.data["fps"] = fps diff --git a/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py b/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py new file mode 100644 index 0000000000..961641b8fa --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/validate_editorial_resources.py @@ -0,0 +1,28 @@ +import os + +import pyblish.api +import pype.api + + +class ValidateEditorialResources(pyblish.api.InstancePlugin): + """Validate there is a "mov" next to the editorial file.""" + + label = "Validate Editorial Resources" + hosts = ["standalonepublisher"] + families = ["editorial"] + order = pype.api.ValidateContentsOrder + + def process(self, instance): + representation = instance.data["representations"][0] + staging_dir = representation["stagingDir"] + basename = os.path.splitext( + os.path.basename(representation["files"]) + )[0] + + files = [x for x in os.listdir(staging_dir)] + + # Check for "mov" file. + filename = basename + ".mov" + filepath = os.path.join(staging_dir, filename) + msg = f"Missing \"{filepath}\"." + assert filename in files, msg diff --git a/pype/plugins/standalonepublisher/publish/validate_shots.py b/pype/plugins/standalonepublisher/publish/validate_shots.py new file mode 100644 index 0000000000..3267af7685 --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/validate_shots.py @@ -0,0 +1,23 @@ +import pyblish.api +import pype.api + + +class ValidateShots(pyblish.api.ContextPlugin): + """Validate there is a "mov" next to the editorial file.""" + + label = "Validate Shots" + hosts = ["standalonepublisher"] + order = pype.api.ValidateContentsOrder + + def process(self, context): + shot_names = [] + duplicate_names = [] + for instance in context: + name = instance.data["name"] + if name in shot_names: + duplicate_names.append(name) + else: + shot_names.append(name) + + msg = "There are duplicate shot names:\n{}".format(duplicate_names) + assert not duplicate_names, msg diff --git a/pype/tools/pyblish_pype/app.css b/pype/tools/pyblish_pype/app.css index b52d9efec8..3a2c05c1f3 100644 --- a/pype/tools/pyblish_pype/app.css +++ b/pype/tools/pyblish_pype/app.css @@ -491,3 +491,24 @@ QToolButton { #TerminalFilerBtn[type="log_critical"]:checked {color: rgb(255, 79, 117);} #TerminalFilerBtn[type="log_critical"] {color: rgba(255, 79, 117, 63);} + +#SuspendLogsBtn { + background: #444; + border: none; + border-top-right-radius: 7px; + border-bottom-right-radius: 7px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + font-family: "FontAwesome"; + font-size: 11pt; + color: white; + padding: 0px; +} + +#SuspendLogsBtn:hover { + background: #333; +} + +#SuspendLogsBtn:disabled { + background: #4c4c4c; +} diff --git a/pype/tools/pyblish_pype/app.py b/pype/tools/pyblish_pype/app.py index 8b77d2f93d..0f662d5b3e 100644 --- a/pype/tools/pyblish_pype/app.py +++ b/pype/tools/pyblish_pype/app.py @@ -5,7 +5,7 @@ import os import sys from . import compat, control, settings, util, window -from .vendor.Qt import QtCore, QtGui, QtWidgets +from Qt import QtCore, QtGui, QtWidgets self = sys.modules[__name__] diff --git a/pype/tools/pyblish_pype/constants.py b/pype/tools/pyblish_pype/constants.py index 077d93eec0..5395d1fd0a 100644 --- a/pype/tools/pyblish_pype/constants.py +++ b/pype/tools/pyblish_pype/constants.py @@ -1,4 +1,4 @@ -from .vendor.Qt import QtCore +from Qt import QtCore def flags(*args, **kwargs): diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index e64f3d5bfb..5138b5cc4c 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -8,8 +8,9 @@ an active window manager; such as via Travis-CI. import os import sys import traceback +import inspect -from .vendor.Qt import QtCore +from Qt import QtCore import pyblish.api import pyblish.util @@ -19,10 +20,8 @@ import pyblish.version from . import util from .constants import InstanceStates -try: - from pypeapp.config import get_presets -except Exception: - get_presets = dict + +from pype.api import config class IterationBreak(Exception): @@ -62,11 +61,15 @@ class Controller(QtCore.QObject): # store OrderGroups - now it is a singleton order_groups = util.OrderGroups + # When instance is toggled + instance_toggled = QtCore.Signal(object, object, object) + def __init__(self, parent=None): super(Controller, self).__init__(parent) self.context = None self.plugins = {} self.optional_default = {} + self.instance_toggled.connect(self._on_instance_toggled) def reset_variables(self): # Data internal to the GUI itself @@ -83,7 +86,6 @@ class Controller(QtCore.QObject): # - passing collectors order disables plugin/instance toggle self.collectors_order = None self.collect_state = 0 - self.collected = False # - passing validators order disables validate button and gives ability # to know when to stop on validate button press @@ -114,7 +116,7 @@ class Controller(QtCore.QObject): def presets_by_hosts(self): # Get global filters as base - presets = get_presets().get("plugins", {}) + presets = config.get_presets().get("plugins", {}) if not presets: return {} @@ -304,6 +306,11 @@ class Controller(QtCore.QObject): "%s was inactive, skipping.." % instance ) continue + # Stop if was stopped + if self.stopped: + self.stopped = False + yield IterationBreak("Stopped") + yield (plugin, instance) else: families = util.collect_families_from_instances( @@ -412,3 +419,19 @@ class Controller(QtCore.QObject): for plugin in self.plugins: del(plugin) + + def _on_instance_toggled(self, instance, old_value, new_value): + callbacks = pyblish.api.registered_callbacks().get("instanceToggled") + if not callbacks: + return + + for callback in callbacks: + try: + callback(instance, old_value, new_value) + except Exception: + print( + "Callback for `instanceToggled` crashed. {}".format( + os.path.abspath(inspect.getfile(callback)) + ) + ) + traceback.print_exception(*sys.exc_info()) diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py index 849495cdeb..e88835b81a 100644 --- a/pype/tools/pyblish_pype/delegate.py +++ b/pype/tools/pyblish_pype/delegate.py @@ -1,6 +1,6 @@ import platform -from .vendor.Qt import QtWidgets, QtGui, QtCore +from Qt import QtWidgets, QtGui, QtCore from . import model from .awesome import tags as awesome diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 58ab3ed0b7..203b512d12 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -29,18 +29,14 @@ import pyblish from . import settings, util from .awesome import tags as awesome -from .vendor import Qt -from .vendor.Qt import QtCore, QtGui -from .vendor.six import text_type -from .vendor.six.moves import queue +import Qt +from Qt import QtCore, QtGui +from six import text_type from .vendor import qtawesome from .constants import PluginStates, InstanceStates, GroupStates, Roles -try: - from pypeapp import config - get_presets = config.get_presets -except Exception: - get_presets = dict +from pype.api import config + # ItemTypes InstanceType = QtGui.QStandardItem.UserType @@ -52,6 +48,7 @@ TerminalDetailType = QtGui.QStandardItem.UserType + 4 class QAwesomeTextIconFactory: icons = {} + @classmethod def icon(cls, icon_name): if icon_name not in cls.icons: @@ -61,6 +58,7 @@ class QAwesomeTextIconFactory: class QAwesomeIconFactory: icons = {} + @classmethod def icon(cls, icon_name, icon_color): if icon_name not in cls.icons: @@ -106,7 +104,7 @@ class IntentModel(QtGui.QStandardItemModel): self.default_index = 0 intents_preset = ( - get_presets() + config.get_presets() .get("tools", {}) .get("pyblish", {}) .get("ui", {}) @@ -492,12 +490,8 @@ class PluginModel(QtGui.QStandardItemModel): new_records = result.get("records") or [] if not has_warning: for record in new_records: - if not hasattr(record, "levelname"): - continue - - if str(record.levelname).lower() in [ - "warning", "critical", "error" - ]: + level_no = record.get("levelno") + if level_no and level_no >= 30: new_flag_states[PluginStates.HasWarning] = True break @@ -791,12 +785,8 @@ class InstanceModel(QtGui.QStandardItemModel): new_records = result.get("records") or [] if not has_warning: for record in new_records: - if not hasattr(record, "levelname"): - continue - - if str(record.levelname).lower() in [ - "warning", "critical", "error" - ]: + level_no = record.get("levelno") + if level_no and level_no >= 30: new_flag_states[InstanceStates.HasWarning] = True break @@ -1012,7 +1002,7 @@ class ArtistProxy(QtCore.QAbstractProxyModel): return QtCore.QModelIndex() -class TerminalModel(QtGui.QStandardItemModel): +class TerminalDetailItem(QtGui.QStandardItem): key_label_record_map = ( ("instance", "Instance"), ("msg", "Message"), @@ -1025,6 +1015,57 @@ class TerminalModel(QtGui.QStandardItemModel): ("msecs", "Millis") ) + def __init__(self, record_item): + self.record_item = record_item + self.msg = None + msg = record_item.get("msg") + if msg is None: + msg = record_item["label"].split("\n")[0] + + super(TerminalDetailItem, self).__init__(msg) + + def data(self, role=QtCore.Qt.DisplayRole): + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if self.msg is None: + self.msg = self.compute_detail_text(self.record_item) + return self.msg + return super(TerminalDetailItem, self).data(role) + + def compute_detail_text(self, item_data): + if item_data["type"] == "info": + return item_data["label"] + + html_text = "" + for key, title in self.key_label_record_map: + if key not in item_data: + continue + value = item_data[key] + text = ( + str(value) + .replace("<", "<") + .replace(">", ">") + .replace('\n', '
') + .replace(' ', ' ') + ) + + title_tag = ( + '{}: ' + ' color:#fff;\" >{}: ' + ).format(title) + + html_text += ( + '{}' + '{}' + ).format(title_tag, text) + + html_text = '{}
'.format( + html_text + ) + return html_text + + +class TerminalModel(QtGui.QStandardItemModel): item_icon_name = { "info": "fa.info", "record": "fa.circle", @@ -1056,38 +1097,38 @@ class TerminalModel(QtGui.QStandardItemModel): self.reset() def reset(self): - self.items_to_set_widget = queue.Queue() self.clear() - def prepare_records(self, result): + def prepare_records(self, result, suspend_logs): prepared_records = [] instance_name = None instance = result["instance"] if instance is not None: instance_name = instance.data["name"] - for record in result.get("records") or []: - if isinstance(record, dict): - record_item = record - else: - record_item = { - "label": text_type(record.msg), - "type": "record", - "levelno": record.levelno, - "threadName": record.threadName, - "name": record.name, - "filename": record.filename, - "pathname": record.pathname, - "lineno": record.lineno, - "msg": text_type(record.msg), - "msecs": record.msecs, - "levelname": record.levelname - } + if not suspend_logs: + for record in result.get("records") or []: + if isinstance(record, dict): + record_item = record + else: + record_item = { + "label": text_type(record.msg), + "type": "record", + "levelno": record.levelno, + "threadName": record.threadName, + "name": record.name, + "filename": record.filename, + "pathname": record.pathname, + "lineno": record.lineno, + "msg": text_type(record.msg), + "msecs": record.msecs, + "levelname": record.levelname + } - if instance_name is not None: - record_item["instance"] = instance_name + if instance_name is not None: + record_item["instance"] = instance_name - prepared_records.append(record_item) + prepared_records.append(record_item) error = result.get("error") if error: @@ -1143,49 +1184,14 @@ class TerminalModel(QtGui.QStandardItemModel): self.appendRow(top_item) - detail_text = self.prepare_detail_text(record_item) - detail_item = QtGui.QStandardItem(detail_text) + detail_item = TerminalDetailItem(record_item) detail_item.setData(TerminalDetailType, Roles.TypeRole) top_item.appendRow(detail_item) - self.items_to_set_widget.put(detail_item) def update_with_result(self, result): for record in result["records"]: self.append(record) - def prepare_detail_text(self, item_data): - if item_data["type"] == "info": - return item_data["label"] - - html_text = "" - for key, title in self.key_label_record_map: - if key not in item_data: - continue - value = item_data[key] - text = ( - str(value) - .replace("<", "<") - .replace(">", ">") - .replace('\n', '
') - .replace(' ', ' ') - ) - - title_tag = ( - '{}: ' - ' color:#fff;\" >{}: ' - ).format(title) - - html_text += ( - '{}' - '{}' - ).format(title_tag, text) - - html_text = '{}
'.format( - html_text - ) - return html_text - class TerminalProxy(QtCore.QSortFilterProxyModel): filter_buttons_checks = { diff --git a/pype/tools/pyblish_pype/util.py b/pype/tools/pyblish_pype/util.py index 82bf4eb51d..d10e7a002a 100644 --- a/pype/tools/pyblish_pype/util.py +++ b/pype/tools/pyblish_pype/util.py @@ -7,8 +7,8 @@ import numbers import copy import collections -from .vendor.Qt import QtCore -from .vendor.six import text_type +from Qt import QtCore +from six import text_type import pyblish.api root = os.path.dirname(__file__) diff --git a/pype/tools/pyblish_pype/vendor/Qt.py b/pype/tools/pyblish_pype/vendor/Qt.py deleted file mode 100644 index 841c823c5c..0000000000 --- a/pype/tools/pyblish_pype/vendor/Qt.py +++ /dev/null @@ -1,1827 +0,0 @@ -"""Minimal Python 2 & 3 shim around all Qt bindings - -DOCUMENTATION - Qt.py was born in the film and visual effects industry to address - the growing need for the development of software capable of running - with more than one flavour of the Qt bindings for Python - PySide, - PySide2, PyQt4 and PyQt5. - - 1. Build for one, run with all - 2. Explicit is better than implicit - 3. Support co-existence - - Default resolution order: - - PySide2 - - PyQt5 - - PySide - - PyQt4 - - Usage: - >> import sys - >> from Qt import QtWidgets - >> app = QtWidgets.QApplication(sys.argv) - >> button = QtWidgets.QPushButton("Hello World") - >> button.show() - >> app.exec_() - - All members of PySide2 are mapped from other bindings, should they exist. - If no equivalent member exist, it is excluded from Qt.py and inaccessible. - The idea is to highlight members that exist across all supported binding, - and guarantee that code that runs on one binding runs on all others. - - For more details, visit https://github.com/mottosso/Qt.py - -LICENSE - - See end of file for license (MIT, BSD) information. - -""" - -import os -import sys -import types -import shutil - - -__version__ = "1.2.0.b2" - -# Enable support for `from Qt import *` -__all__ = [] - -# Flags from environment variables -QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) -QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") -QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") - -# Reference to Qt.py -Qt = sys.modules[__name__] -Qt.QtCompat = types.ModuleType("QtCompat") - -try: - long -except NameError: - # Python 3 compatibility - long = int - - -"""Common members of all bindings - -This is where each member of Qt.py is explicitly defined. -It is based on a "lowest common denominator" of all bindings; -including members found in each of the 4 bindings. - -The "_common_members" dictionary is generated using the -build_membership.sh script. - -""" - -_common_members = { - "QtCore": [ - "QAbstractAnimation", - "QAbstractEventDispatcher", - "QAbstractItemModel", - "QAbstractListModel", - "QAbstractState", - "QAbstractTableModel", - "QAbstractTransition", - "QAnimationGroup", - "QBasicTimer", - "QBitArray", - "QBuffer", - "QByteArray", - "QByteArrayMatcher", - "QChildEvent", - "QCoreApplication", - "QCryptographicHash", - "QDataStream", - "QDate", - "QDateTime", - "QDir", - "QDirIterator", - "QDynamicPropertyChangeEvent", - "QEasingCurve", - "QElapsedTimer", - "QEvent", - "QEventLoop", - "QEventTransition", - "QFile", - "QFileInfo", - "QFileSystemWatcher", - "QFinalState", - "QGenericArgument", - "QGenericReturnArgument", - "QHistoryState", - "QItemSelectionRange", - "QIODevice", - "QLibraryInfo", - "QLine", - "QLineF", - "QLocale", - "QMargins", - "QMetaClassInfo", - "QMetaEnum", - "QMetaMethod", - "QMetaObject", - "QMetaProperty", - "QMimeData", - "QModelIndex", - "QMutex", - "QMutexLocker", - "QObject", - "QParallelAnimationGroup", - "QPauseAnimation", - "QPersistentModelIndex", - "QPluginLoader", - "QPoint", - "QPointF", - "QProcess", - "QProcessEnvironment", - "QPropertyAnimation", - "QReadLocker", - "QReadWriteLock", - "QRect", - "QRectF", - "QRegExp", - "QResource", - "QRunnable", - "QSemaphore", - "QSequentialAnimationGroup", - "QSettings", - "QSignalMapper", - "QSignalTransition", - "QSize", - "QSizeF", - "QSocketNotifier", - "QState", - "QStateMachine", - "QSysInfo", - "QSystemSemaphore", - "QT_TRANSLATE_NOOP", - "QT_TR_NOOP", - "QT_TR_NOOP_UTF8", - "QTemporaryFile", - "QTextBoundaryFinder", - "QTextCodec", - "QTextDecoder", - "QTextEncoder", - "QTextStream", - "QTextStreamManipulator", - "QThread", - "QThreadPool", - "QTime", - "QTimeLine", - "QTimer", - "QTimerEvent", - "QTranslator", - "QUrl", - "QVariantAnimation", - "QWaitCondition", - "QWriteLocker", - "QXmlStreamAttribute", - "QXmlStreamAttributes", - "QXmlStreamEntityDeclaration", - "QXmlStreamEntityResolver", - "QXmlStreamNamespaceDeclaration", - "QXmlStreamNotationDeclaration", - "QXmlStreamReader", - "QXmlStreamWriter", - "Qt", - "QtCriticalMsg", - "QtDebugMsg", - "QtFatalMsg", - "QtMsgType", - "QtSystemMsg", - "QtWarningMsg", - "qAbs", - "qAddPostRoutine", - "qChecksum", - "qCritical", - "qDebug", - "qFatal", - "qFuzzyCompare", - "qIsFinite", - "qIsInf", - "qIsNaN", - "qIsNull", - "qRegisterResourceData", - "qUnregisterResourceData", - "qVersion", - "qWarning", - "qrand", - "qsrand" - ], - "QtGui": [ - "QAbstractTextDocumentLayout", - "QActionEvent", - "QBitmap", - "QBrush", - "QClipboard", - "QCloseEvent", - "QColor", - "QConicalGradient", - "QContextMenuEvent", - "QCursor", - "QDesktopServices", - "QDoubleValidator", - "QDrag", - "QDragEnterEvent", - "QDragLeaveEvent", - "QDragMoveEvent", - "QDropEvent", - "QFileOpenEvent", - "QFocusEvent", - "QFont", - "QFontDatabase", - "QFontInfo", - "QFontMetrics", - "QFontMetricsF", - "QGradient", - "QHelpEvent", - "QHideEvent", - "QHoverEvent", - "QIcon", - "QIconDragEvent", - "QIconEngine", - "QImage", - "QImageIOHandler", - "QImageReader", - "QImageWriter", - "QInputEvent", - "QInputMethodEvent", - "QIntValidator", - "QKeyEvent", - "QKeySequence", - "QLinearGradient", - "QMatrix2x2", - "QMatrix2x3", - "QMatrix2x4", - "QMatrix3x2", - "QMatrix3x3", - "QMatrix3x4", - "QMatrix4x2", - "QMatrix4x3", - "QMatrix4x4", - "QMouseEvent", - "QMoveEvent", - "QMovie", - "QPaintDevice", - "QPaintEngine", - "QPaintEngineState", - "QPaintEvent", - "QPainter", - "QPainterPath", - "QPainterPathStroker", - "QPalette", - "QPen", - "QPicture", - "QPictureIO", - "QPixmap", - "QPixmapCache", - "QPolygon", - "QPolygonF", - "QQuaternion", - "QRadialGradient", - "QRegExpValidator", - "QRegion", - "QResizeEvent", - "QSessionManager", - "QShortcutEvent", - "QShowEvent", - "QStandardItem", - "QStandardItemModel", - "QStatusTipEvent", - "QSyntaxHighlighter", - "QTabletEvent", - "QTextBlock", - "QTextBlockFormat", - "QTextBlockGroup", - "QTextBlockUserData", - "QTextCharFormat", - "QTextCursor", - "QTextDocument", - "QTextDocumentFragment", - "QTextFormat", - "QTextFragment", - "QTextFrame", - "QTextFrameFormat", - "QTextImageFormat", - "QTextInlineObject", - "QTextItem", - "QTextLayout", - "QTextLength", - "QTextLine", - "QTextList", - "QTextListFormat", - "QTextObject", - "QTextObjectInterface", - "QTextOption", - "QTextTable", - "QTextTableCell", - "QTextTableCellFormat", - "QTextTableFormat", - "QTouchEvent", - "QTransform", - "QValidator", - "QVector2D", - "QVector3D", - "QVector4D", - "QWhatsThisClickedEvent", - "QWheelEvent", - "QWindowStateChangeEvent", - "qAlpha", - "qBlue", - "qGray", - "qGreen", - "qIsGray", - "qRed", - "qRgb", - "qRgba" - ], - "QtHelp": [ - "QHelpContentItem", - "QHelpContentModel", - "QHelpContentWidget", - "QHelpEngine", - "QHelpEngineCore", - "QHelpIndexModel", - "QHelpIndexWidget", - "QHelpSearchEngine", - "QHelpSearchQuery", - "QHelpSearchQueryWidget", - "QHelpSearchResultWidget" - ], - "QtMultimedia": [ - "QAbstractVideoBuffer", - "QAbstractVideoSurface", - "QAudio", - "QAudioDeviceInfo", - "QAudioFormat", - "QAudioInput", - "QAudioOutput", - "QVideoFrame", - "QVideoSurfaceFormat" - ], - "QtNetwork": [ - "QAbstractNetworkCache", - "QAbstractSocket", - "QAuthenticator", - "QHostAddress", - "QHostInfo", - "QLocalServer", - "QLocalSocket", - "QNetworkAccessManager", - "QNetworkAddressEntry", - "QNetworkCacheMetaData", - "QNetworkConfiguration", - "QNetworkConfigurationManager", - "QNetworkCookie", - "QNetworkCookieJar", - "QNetworkDiskCache", - "QNetworkInterface", - "QNetworkProxy", - "QNetworkProxyFactory", - "QNetworkProxyQuery", - "QNetworkReply", - "QNetworkRequest", - "QNetworkSession", - "QSsl", - "QTcpServer", - "QTcpSocket", - "QUdpSocket" - ], - "QtOpenGL": [ - "QGL", - "QGLContext", - "QGLFormat", - "QGLWidget" - ], - "QtPrintSupport": [ - "QAbstractPrintDialog", - "QPageSetupDialog", - "QPrintDialog", - "QPrintEngine", - "QPrintPreviewDialog", - "QPrintPreviewWidget", - "QPrinter", - "QPrinterInfo" - ], - "QtSql": [ - "QSql", - "QSqlDatabase", - "QSqlDriver", - "QSqlDriverCreatorBase", - "QSqlError", - "QSqlField", - "QSqlIndex", - "QSqlQuery", - "QSqlQueryModel", - "QSqlRecord", - "QSqlRelation", - "QSqlRelationalDelegate", - "QSqlRelationalTableModel", - "QSqlResult", - "QSqlTableModel" - ], - "QtSvg": [ - "QGraphicsSvgItem", - "QSvgGenerator", - "QSvgRenderer", - "QSvgWidget" - ], - "QtTest": [ - "QTest" - ], - "QtWidgets": [ - "QAbstractButton", - "QAbstractGraphicsShapeItem", - "QAbstractItemDelegate", - "QAbstractItemView", - "QAbstractScrollArea", - "QAbstractSlider", - "QAbstractSpinBox", - "QAction", - "QActionGroup", - "QApplication", - "QBoxLayout", - "QButtonGroup", - "QCalendarWidget", - "QCheckBox", - "QColorDialog", - "QColumnView", - "QComboBox", - "QCommandLinkButton", - "QCommonStyle", - "QCompleter", - "QDataWidgetMapper", - "QDateEdit", - "QDateTimeEdit", - "QDesktopWidget", - "QDial", - "QDialog", - "QDialogButtonBox", - "QDirModel", - "QDockWidget", - "QDoubleSpinBox", - "QErrorMessage", - "QFileDialog", - "QFileIconProvider", - "QFileSystemModel", - "QFocusFrame", - "QFontComboBox", - "QFontDialog", - "QFormLayout", - "QFrame", - "QGesture", - "QGestureEvent", - "QGestureRecognizer", - "QGraphicsAnchor", - "QGraphicsAnchorLayout", - "QGraphicsBlurEffect", - "QGraphicsColorizeEffect", - "QGraphicsDropShadowEffect", - "QGraphicsEffect", - "QGraphicsEllipseItem", - "QGraphicsGridLayout", - "QGraphicsItem", - "QGraphicsItemGroup", - "QGraphicsLayout", - "QGraphicsLayoutItem", - "QGraphicsLineItem", - "QGraphicsLinearLayout", - "QGraphicsObject", - "QGraphicsOpacityEffect", - "QGraphicsPathItem", - "QGraphicsPixmapItem", - "QGraphicsPolygonItem", - "QGraphicsProxyWidget", - "QGraphicsRectItem", - "QGraphicsRotation", - "QGraphicsScale", - "QGraphicsScene", - "QGraphicsSceneContextMenuEvent", - "QGraphicsSceneDragDropEvent", - "QGraphicsSceneEvent", - "QGraphicsSceneHelpEvent", - "QGraphicsSceneHoverEvent", - "QGraphicsSceneMouseEvent", - "QGraphicsSceneMoveEvent", - "QGraphicsSceneResizeEvent", - "QGraphicsSceneWheelEvent", - "QGraphicsSimpleTextItem", - "QGraphicsTextItem", - "QGraphicsTransform", - "QGraphicsView", - "QGraphicsWidget", - "QGridLayout", - "QGroupBox", - "QHBoxLayout", - "QHeaderView", - "QInputDialog", - "QItemDelegate", - "QItemEditorCreatorBase", - "QItemEditorFactory", - "QKeyEventTransition", - "QLCDNumber", - "QLabel", - "QLayout", - "QLayoutItem", - "QLineEdit", - "QListView", - "QListWidget", - "QListWidgetItem", - "QMainWindow", - "QMdiArea", - "QMdiSubWindow", - "QMenu", - "QMenuBar", - "QMessageBox", - "QMouseEventTransition", - "QPanGesture", - "QPinchGesture", - "QPlainTextDocumentLayout", - "QPlainTextEdit", - "QProgressBar", - "QProgressDialog", - "QPushButton", - "QRadioButton", - "QRubberBand", - "QScrollArea", - "QScrollBar", - "QShortcut", - "QSizeGrip", - "QSizePolicy", - "QSlider", - "QSpacerItem", - "QSpinBox", - "QSplashScreen", - "QSplitter", - "QSplitterHandle", - "QStackedLayout", - "QStackedWidget", - "QStatusBar", - "QStyle", - "QStyleFactory", - "QStyleHintReturn", - "QStyleHintReturnMask", - "QStyleHintReturnVariant", - "QStyleOption", - "QStyleOptionButton", - "QStyleOptionComboBox", - "QStyleOptionComplex", - "QStyleOptionDockWidget", - "QStyleOptionFocusRect", - "QStyleOptionFrame", - "QStyleOptionGraphicsItem", - "QStyleOptionGroupBox", - "QStyleOptionHeader", - "QStyleOptionMenuItem", - "QStyleOptionProgressBar", - "QStyleOptionRubberBand", - "QStyleOptionSizeGrip", - "QStyleOptionSlider", - "QStyleOptionSpinBox", - "QStyleOptionTab", - "QStyleOptionTabBarBase", - "QStyleOptionTabWidgetFrame", - "QStyleOptionTitleBar", - "QStyleOptionToolBar", - "QStyleOptionToolBox", - "QStyleOptionToolButton", - "QStyleOptionViewItem", - "QStylePainter", - "QStyledItemDelegate", - "QSwipeGesture", - "QSystemTrayIcon", - "QTabBar", - "QTabWidget", - "QTableView", - "QTableWidget", - "QTableWidgetItem", - "QTableWidgetSelectionRange", - "QTapAndHoldGesture", - "QTapGesture", - "QTextBrowser", - "QTextEdit", - "QTimeEdit", - "QToolBar", - "QToolBox", - "QToolButton", - "QToolTip", - "QTreeView", - "QTreeWidget", - "QTreeWidgetItem", - "QTreeWidgetItemIterator", - "QUndoCommand", - "QUndoGroup", - "QUndoStack", - "QUndoView", - "QVBoxLayout", - "QWhatsThis", - "QWidget", - "QWidgetAction", - "QWidgetItem", - "QWizard", - "QWizardPage" - ], - "QtX11Extras": [ - "QX11Info" - ], - "QtXml": [ - "QDomAttr", - "QDomCDATASection", - "QDomCharacterData", - "QDomComment", - "QDomDocument", - "QDomDocumentFragment", - "QDomDocumentType", - "QDomElement", - "QDomEntity", - "QDomEntityReference", - "QDomImplementation", - "QDomNamedNodeMap", - "QDomNode", - "QDomNodeList", - "QDomNotation", - "QDomProcessingInstruction", - "QDomText", - "QXmlAttributes", - "QXmlContentHandler", - "QXmlDTDHandler", - "QXmlDeclHandler", - "QXmlDefaultHandler", - "QXmlEntityResolver", - "QXmlErrorHandler", - "QXmlInputSource", - "QXmlLexicalHandler", - "QXmlLocator", - "QXmlNamespaceSupport", - "QXmlParseException", - "QXmlReader", - "QXmlSimpleReader" - ], - "QtXmlPatterns": [ - "QAbstractMessageHandler", - "QAbstractUriResolver", - "QAbstractXmlNodeModel", - "QAbstractXmlReceiver", - "QSourceLocation", - "QXmlFormatter", - "QXmlItem", - "QXmlName", - "QXmlNamePool", - "QXmlNodeModelIndex", - "QXmlQuery", - "QXmlResultItems", - "QXmlSchema", - "QXmlSchemaValidator", - "QXmlSerializer" - ] -} - - -def _qInstallMessageHandler(handler): - """Install a message handler that works in all bindings - - Args: - handler: A function that takes 3 arguments, or None - """ - def messageOutputHandler(*args): - # In Qt4 bindings, message handlers are passed 2 arguments - # In Qt5 bindings, message handlers are passed 3 arguments - # The first argument is a QtMsgType - # The last argument is the message to be printed - # The Middle argument (if passed) is a QMessageLogContext - if len(args) == 3: - msgType, logContext, msg = args - elif len(args) == 2: - msgType, msg = args - logContext = None - else: - raise TypeError( - "handler expected 2 or 3 arguments, got {0}".format(len(args))) - - if isinstance(msg, bytes): - # In python 3, some bindings pass a bytestring, which cannot be - # used elsewhere. Decoding a python 2 or 3 bytestring object will - # consistently return a unicode object. - msg = msg.decode() - - handler(msgType, logContext, msg) - - passObject = messageOutputHandler if handler else handler - if Qt.IsPySide or Qt.IsPyQt4: - return Qt._QtCore.qInstallMsgHandler(passObject) - elif Qt.IsPySide2 or Qt.IsPyQt5: - return Qt._QtCore.qInstallMessageHandler(passObject) - - -def _getcpppointer(object): - if hasattr(Qt, "_shiboken2"): - return getattr(Qt, "_shiboken2").getCppPointer(object)[0] - elif hasattr(Qt, "_shiboken"): - return getattr(Qt, "_shiboken").getCppPointer(object)[0] - elif hasattr(Qt, "_sip"): - return getattr(Qt, "_sip").unwrapinstance(object) - raise AttributeError("'module' has no attribute 'getCppPointer'") - - -def _wrapinstance(ptr, base=None): - """Enable implicit cast of pointer to most suitable class - - This behaviour is available in sip per default. - - Based on http://nathanhorne.com/pyqtpyside-wrap-instance - - Usage: - This mechanism kicks in under these circumstances. - 1. Qt.py is using PySide 1 or 2. - 2. A `base` argument is not provided. - - See :func:`QtCompat.wrapInstance()` - - Arguments: - ptr (long): Pointer to QObject in memory - base (QObject, optional): Base class to wrap with. Defaults to QObject, - which should handle anything. - - """ - - assert isinstance(ptr, long), "Argument 'ptr' must be of type " - assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( - "Argument 'base' must be of type ") - - if Qt.IsPyQt4 or Qt.IsPyQt5: - func = getattr(Qt, "_sip").wrapinstance - elif Qt.IsPySide2: - func = getattr(Qt, "_shiboken2").wrapInstance - elif Qt.IsPySide: - func = getattr(Qt, "_shiboken").wrapInstance - else: - raise AttributeError("'module' has no attribute 'wrapInstance'") - - if base is None: - q_object = func(long(ptr), Qt.QtCore.QObject) - meta_object = q_object.metaObject() - class_name = meta_object.className() - super_class_name = meta_object.superClass().className() - - if hasattr(Qt.QtWidgets, class_name): - base = getattr(Qt.QtWidgets, class_name) - - elif hasattr(Qt.QtWidgets, super_class_name): - base = getattr(Qt.QtWidgets, super_class_name) - - else: - base = Qt.QtCore.QObject - - return func(long(ptr), base) - - -def _translate(context, sourceText, *args): - # In Qt4 bindings, translate can be passed 2 or 3 arguments - # In Qt5 bindings, translate can be passed 2 arguments - # The first argument is disambiguation[str] - # The last argument is n[int] - # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] - if len(args) == 3: - disambiguation, encoding, n = args - elif len(args) == 2: - disambiguation, n = args - encoding = None - else: - raise TypeError( - "Expected 4 or 5 arguments, got {0}.".format(len(args)+2)) - - if hasattr(Qt.QtCore, "QCoreApplication"): - app = getattr(Qt.QtCore, "QCoreApplication") - else: - raise NotImplementedError( - "Missing QCoreApplication implementation for {binding}".format( - binding=Qt.__binding__, - ) - ) - if Qt.__binding__ in ("PySide2", "PyQt5"): - sanitized_args = [context, sourceText, disambiguation, n] - else: - sanitized_args = [ - context, - sourceText, - disambiguation, - encoding or app.CodecForTr, - n - ] - return app.translate(*sanitized_args) - - -def _loadUi(uifile, baseinstance=None): - """Dynamically load a user interface from the given `uifile` - - This function calls `uic.loadUi` if using PyQt bindings, - else it implements a comparable binding for PySide. - - Documentation: - http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi - - Arguments: - uifile (str): Absolute path to Qt Designer file. - baseinstance (QWidget): Instantiated QWidget or subclass thereof - - Return: - baseinstance if `baseinstance` is not `None`. Otherwise - return the newly created instance of the user interface. - - """ - if hasattr(Qt, "_uic"): - return Qt._uic.loadUi(uifile, baseinstance) - - elif hasattr(Qt, "_QtUiTools"): - # Implement `PyQt5.uic.loadUi` for PySide(2) - - class _UiLoader(Qt._QtUiTools.QUiLoader): - """Create the user interface in a base instance. - - Unlike `Qt._QtUiTools.QUiLoader` itself this class does not - create a new instance of the top-level widget, but creates the user - interface in an existing instance of the top-level class if needed. - - This mimics the behaviour of `PyQt5.uic.loadUi`. - - """ - - def __init__(self, baseinstance): - super(_UiLoader, self).__init__(baseinstance) - self.baseinstance = baseinstance - - def load(self, uifile, *args, **kwargs): - from xml.etree.ElementTree import ElementTree - - # For whatever reason, if this doesn't happen then - # reading an invalid or non-existing .ui file throws - # a RuntimeError. - etree = ElementTree() - etree.parse(uifile) - - widget = Qt._QtUiTools.QUiLoader.load( - self, uifile, *args, **kwargs) - - # Workaround for PySide 1.0.9, see issue #208 - widget.parentWidget() - - return widget - - def createWidget(self, class_name, parent=None, name=""): - """Called for each widget defined in ui file - - Overridden here to populate `baseinstance` instead. - - """ - - if parent is None and self.baseinstance: - # Supposed to create the top-level widget, - # return the base instance instead - return self.baseinstance - - # For some reason, Line is not in the list of available - # widgets, but works fine, so we have to special case it here. - if class_name in self.availableWidgets() + ["Line"]: - # Create a new widget for child widgets - widget = Qt._QtUiTools.QUiLoader.createWidget(self, - class_name, - parent, - name) - - else: - raise Exception("Custom widget '%s' not supported" - % class_name) - - if self.baseinstance: - # Set an attribute for the new child widget on the base - # instance, just like PyQt5.uic.loadUi does. - setattr(self.baseinstance, name, widget) - - return widget - - widget = _UiLoader(baseinstance).load(uifile) - Qt.QtCore.QMetaObject.connectSlotsByName(widget) - - return widget - - else: - raise NotImplementedError("No implementation available for loadUi") - - -"""Misplaced members - -These members from the original submodule are misplaced relative PySide2 - -""" -_misplaced_members = { - "PySide2": { - "QtGui.QStringListModel": "QtCore.QStringListModel", - "QtCore.Property": "QtCore.Property", - "QtCore.Signal": "QtCore.Signal", - "QtCore.Slot": "QtCore.Slot", - "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", - "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", - "QtCore.QItemSelection": "QtCore.QItemSelection", - "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", - "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", - "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], - "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], - "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer], - "QtWidgets.qApp": "QtWidgets.QApplication.instance()", - "QtCore.QCoreApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtWidgets.QApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtCore.qInstallMessageHandler": [ - "QtCompat.qInstallMessageHandler", _qInstallMessageHandler - ], - }, - "PyQt5": { - "QtCore.pyqtProperty": "QtCore.Property", - "QtCore.pyqtSignal": "QtCore.Signal", - "QtCore.pyqtSlot": "QtCore.Slot", - "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel", - "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", - "QtCore.QStringListModel": "QtCore.QStringListModel", - "QtCore.QItemSelection": "QtCore.QItemSelection", - "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", - "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", - "uic.loadUi": ["QtCompat.loadUi", _loadUi], - "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], - "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], - "QtWidgets.qApp": "QtWidgets.QApplication.instance()", - "QtCore.QCoreApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtWidgets.QApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtCore.qInstallMessageHandler": [ - "QtCompat.qInstallMessageHandler", _qInstallMessageHandler - ], - }, - "PySide": { - "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", - "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", - "QtGui.QStringListModel": "QtCore.QStringListModel", - "QtGui.QItemSelection": "QtCore.QItemSelection", - "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", - "QtCore.Property": "QtCore.Property", - "QtCore.Signal": "QtCore.Signal", - "QtCore.Slot": "QtCore.Slot", - "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", - "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", - "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", - "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", - "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", - "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", - "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", - "QtGui.QPrinter": "QtPrintSupport.QPrinter", - "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", - "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], - "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], - "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer], - "QtGui.qApp": "QtWidgets.QApplication.instance()", - "QtCore.QCoreApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtGui.QApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtCore.qInstallMsgHandler": [ - "QtCompat.qInstallMessageHandler", _qInstallMessageHandler - ], - }, - "PyQt4": { - "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", - "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel", - "QtGui.QItemSelection": "QtCore.QItemSelection", - "QtGui.QStringListModel": "QtCore.QStringListModel", - "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel", - "QtCore.pyqtProperty": "QtCore.Property", - "QtCore.pyqtSignal": "QtCore.Signal", - "QtCore.pyqtSlot": "QtCore.Slot", - "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange", - "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog", - "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog", - "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog", - "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine", - "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog", - "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", - "QtGui.QPrinter": "QtPrintSupport.QPrinter", - "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", - # "QtCore.pyqtSignature": "QtCore.Slot", - "uic.loadUi": ["QtCompat.loadUi", _loadUi], - "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], - "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], - "QtCore.QString": "str", - "QtGui.qApp": "QtWidgets.QApplication.instance()", - "QtCore.QCoreApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtGui.QApplication.translate": [ - "QtCompat.translate", _translate - ], - "QtCore.qInstallMsgHandler": [ - "QtCompat.qInstallMessageHandler", _qInstallMessageHandler - ], - } -} - -""" Compatibility Members - -This dictionary is used to build Qt.QtCompat objects that provide a consistent -interface for obsolete members, and differences in binding return values. - -{ - "binding": { - "classname": { - "targetname": "binding_namespace", - } - } -} -""" -_compatibility_members = { - "PySide2": { - "QWidget": { - "grab": "QtWidgets.QWidget.grab", - }, - "QHeaderView": { - "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", - "setSectionsClickable": - "QtWidgets.QHeaderView.setSectionsClickable", - "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", - "setSectionResizeMode": - "QtWidgets.QHeaderView.setSectionResizeMode", - "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", - "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", - }, - "QFileDialog": { - "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", - "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", - "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", - }, - }, - "PyQt5": { - "QWidget": { - "grab": "QtWidgets.QWidget.grab", - }, - "QHeaderView": { - "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", - "setSectionsClickable": - "QtWidgets.QHeaderView.setSectionsClickable", - "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode", - "setSectionResizeMode": - "QtWidgets.QHeaderView.setSectionResizeMode", - "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable", - "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable", - }, - "QFileDialog": { - "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", - "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", - "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", - }, - }, - "PySide": { - "QWidget": { - "grab": "QtWidgets.QPixmap.grabWidget", - }, - "QHeaderView": { - "sectionsClickable": "QtWidgets.QHeaderView.isClickable", - "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", - "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", - "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", - "sectionsMovable": "QtWidgets.QHeaderView.isMovable", - "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", - }, - "QFileDialog": { - "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", - "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", - "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", - }, - }, - "PyQt4": { - "QWidget": { - "grab": "QtWidgets.QPixmap.grabWidget", - }, - "QHeaderView": { - "sectionsClickable": "QtWidgets.QHeaderView.isClickable", - "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", - "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode", - "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode", - "sectionsMovable": "QtWidgets.QHeaderView.isMovable", - "setSectionsMovable": "QtWidgets.QHeaderView.setMovable", - }, - "QFileDialog": { - "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName", - "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames", - "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName", - }, - }, -} - - -def _apply_site_config(): - try: - import QtSiteConfig - except ImportError: - # If no QtSiteConfig module found, no modifications - # to _common_members are needed. - pass - else: - # Provide the ability to modify the dicts used to build Qt.py - if hasattr(QtSiteConfig, 'update_members'): - QtSiteConfig.update_members(_common_members) - - if hasattr(QtSiteConfig, 'update_misplaced_members'): - QtSiteConfig.update_misplaced_members(members=_misplaced_members) - - if hasattr(QtSiteConfig, 'update_compatibility_members'): - QtSiteConfig.update_compatibility_members( - members=_compatibility_members) - - -def _new_module(name): - return types.ModuleType(__name__ + "." + name) - - -def _import_sub_module(module, name): - """import_sub_module will mimic the function of importlib.import_module""" - module = __import__(module.__name__ + "." + name) - for level in name.split("."): - module = getattr(module, level) - return module - - -def _setup(module, extras): - """Install common submodules""" - - Qt.__binding__ = module.__name__ - - for name in list(_common_members) + extras: - try: - submodule = _import_sub_module( - module, name) - except ImportError: - try: - # For extra modules like sip and shiboken that may not be - # children of the binding. - submodule = __import__(name) - except ImportError: - continue - - setattr(Qt, "_" + name, submodule) - - if name not in extras: - # Store reference to original binding, - # but don't store speciality modules - # such as uic or QtUiTools - setattr(Qt, name, _new_module(name)) - - -def _reassign_misplaced_members(binding): - """Apply misplaced members from `binding` to Qt.py - - Arguments: - binding (dict): Misplaced members - - """ - - for src, dst in _misplaced_members[binding].items(): - dst_value = None - - src_parts = src.split(".") - src_module = src_parts[0] - src_member = None - if len(src_parts) > 1: - src_member = src_parts[1:] - - if isinstance(dst, (list, tuple)): - dst, dst_value = dst - - dst_parts = dst.split(".") - dst_module = dst_parts[0] - dst_member = None - if len(dst_parts) > 1: - dst_member = dst_parts[1] - - # Get the member we want to store in the namesapce. - if not dst_value: - try: - _part = getattr(Qt, "_" + src_module) - while src_member: - member = src_member.pop(0) - _part = getattr(_part, member) - dst_value = _part - except AttributeError: - # If the member we want to store in the namespace does not - # exist, there is no need to continue. This can happen if a - # request was made to rename a member that didn't exist, for - # example if QtWidgets isn't available on the target platform. - _log("Misplaced member has no source: {}".format(src)) - continue - - try: - src_object = getattr(Qt, dst_module) - except AttributeError: - if dst_module not in _common_members: - # Only create the Qt parent module if its listed in - # _common_members. Without this check, if you remove QtCore - # from _common_members, the default _misplaced_members will add - # Qt.QtCore so it can add Signal, Slot, etc. - msg = 'Not creating missing member module "{m}" for "{c}"' - _log(msg.format(m=dst_module, c=dst_member)) - continue - # If the dst is valid but the Qt parent module does not exist - # then go ahead and create a new module to contain the member. - setattr(Qt, dst_module, _new_module(dst_module)) - src_object = getattr(Qt, dst_module) - # Enable direct import of the new module - sys.modules[__name__ + "." + dst_module] = src_object - - if not dst_value: - dst_value = getattr(Qt, "_" + src_module) - if src_member: - dst_value = getattr(dst_value, src_member) - - setattr( - src_object, - dst_member or dst_module, - dst_value - ) - - -def _build_compatibility_members(binding, decorators=None): - """Apply `binding` to QtCompat - - Arguments: - binding (str): Top level binding in _compatibility_members. - decorators (dict, optional): Provides the ability to decorate the - original Qt methods when needed by a binding. This can be used - to change the returned value to a standard value. The key should - be the classname, the value is a dict where the keys are the - target method names, and the values are the decorator functions. - - """ - - decorators = decorators or dict() - - # Allow optional site-level customization of the compatibility members. - # This method does not need to be implemented in QtSiteConfig. - try: - import QtSiteConfig - except ImportError: - pass - else: - if hasattr(QtSiteConfig, 'update_compatibility_decorators'): - QtSiteConfig.update_compatibility_decorators(binding, decorators) - - _QtCompat = type("QtCompat", (object,), {}) - - for classname, bindings in _compatibility_members[binding].items(): - attrs = {} - for target, binding in bindings.items(): - namespaces = binding.split('.') - try: - src_object = getattr(Qt, "_" + namespaces[0]) - except AttributeError as e: - _log("QtCompat: AttributeError: %s" % e) - # Skip reassignment of non-existing members. - # This can happen if a request was made to - # rename a member that didn't exist, for example - # if QtWidgets isn't available on the target platform. - continue - - # Walk down any remaining namespace getting the object assuming - # that if the first namespace exists the rest will exist. - for namespace in namespaces[1:]: - src_object = getattr(src_object, namespace) - - # decorate the Qt method if a decorator was provided. - if target in decorators.get(classname, []): - # staticmethod must be called on the decorated method to - # prevent a TypeError being raised when the decorated method - # is called. - src_object = staticmethod( - decorators[classname][target](src_object)) - - attrs[target] = src_object - - # Create the QtCompat class and install it into the namespace - compat_class = type(classname, (_QtCompat,), attrs) - setattr(Qt.QtCompat, classname, compat_class) - - -def _pyside2(): - """Initialise PySide2 - - These functions serve to test the existence of a binding - along with set it up in such a way that it aligns with - the final step; adding members from the original binding - to Qt.py - - """ - - import PySide2 as module - extras = ["QtUiTools"] - try: - try: - # Before merge of PySide and shiboken - import shiboken2 - except ImportError: - # After merge of PySide and shiboken, May 2017 - from PySide2 import shiboken2 - extras.append("shiboken2") - except ImportError: - pass - - _setup(module, extras) - Qt.__binding_version__ = module.__version__ - - if hasattr(Qt, "_shiboken2"): - Qt.QtCompat.wrapInstance = _wrapinstance - Qt.QtCompat.getCppPointer = _getcpppointer - - if hasattr(Qt, "_QtUiTools"): - Qt.QtCompat.loadUi = _loadUi - - if hasattr(Qt, "_QtCore"): - Qt.__qt_version__ = Qt._QtCore.qVersion() - - if hasattr(Qt, "_QtWidgets"): - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtWidgets.QHeaderView.setSectionResizeMode - - _reassign_misplaced_members("PySide2") - _build_compatibility_members("PySide2") - - -def _pyside(): - """Initialise PySide""" - - import PySide as module - extras = ["QtUiTools"] - try: - try: - # Before merge of PySide and shiboken - import shiboken - except ImportError: - # After merge of PySide and shiboken, May 2017 - from PySide import shiboken - extras.append("shiboken") - except ImportError: - pass - - _setup(module, extras) - Qt.__binding_version__ = module.__version__ - - if hasattr(Qt, "_shiboken"): - Qt.QtCompat.wrapInstance = _wrapinstance - Qt.QtCompat.getCppPointer = _getcpppointer - - if hasattr(Qt, "_QtUiTools"): - Qt.QtCompat.loadUi = _loadUi - - if hasattr(Qt, "_QtGui"): - setattr(Qt, "QtWidgets", _new_module("QtWidgets")) - setattr(Qt, "_QtWidgets", Qt._QtGui) - if hasattr(Qt._QtGui, "QX11Info"): - setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) - Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info - - Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode - - if hasattr(Qt, "_QtCore"): - Qt.__qt_version__ = Qt._QtCore.qVersion() - - _reassign_misplaced_members("PySide") - _build_compatibility_members("PySide") - - -def _pyqt5(): - """Initialise PyQt5""" - - import PyQt5 as module - extras = ["uic"] - try: - import sip - extras.append(sip.__name__) - except ImportError: - sip = None - - _setup(module, extras) - if hasattr(Qt, "_sip"): - Qt.QtCompat.wrapInstance = _wrapinstance - Qt.QtCompat.getCppPointer = _getcpppointer - - if hasattr(Qt, "_uic"): - Qt.QtCompat.loadUi = _loadUi - - if hasattr(Qt, "_QtCore"): - Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR - Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR - - if hasattr(Qt, "_QtWidgets"): - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtWidgets.QHeaderView.setSectionResizeMode - - _reassign_misplaced_members("PyQt5") - _build_compatibility_members('PyQt5') - - -def _pyqt4(): - """Initialise PyQt4""" - - import sip - - # Validation of envivornment variable. Prevents an error if - # the variable is invalid since it's just a hint. - try: - hint = int(QT_SIP_API_HINT) - except TypeError: - hint = None # Variable was None, i.e. not set. - except ValueError: - raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2") - - for api in ("QString", - "QVariant", - "QDate", - "QDateTime", - "QTextStream", - "QTime", - "QUrl"): - try: - sip.setapi(api, hint or 2) - except AttributeError: - raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py") - except ValueError: - actual = sip.getapi(api) - if not hint: - raise ImportError("API version already set to %d" % actual) - else: - # Having provided a hint indicates a soft constraint, one - # that doesn't throw an exception. - sys.stderr.write( - "Warning: API '%s' has already been set to %d.\n" - % (api, actual) - ) - - import PyQt4 as module - extras = ["uic"] - try: - import sip - extras.append(sip.__name__) - except ImportError: - sip = None - - _setup(module, extras) - if hasattr(Qt, "_sip"): - Qt.QtCompat.wrapInstance = _wrapinstance - Qt.QtCompat.getCppPointer = _getcpppointer - - if hasattr(Qt, "_uic"): - Qt.QtCompat.loadUi = _loadUi - - if hasattr(Qt, "_QtGui"): - setattr(Qt, "QtWidgets", _new_module("QtWidgets")) - setattr(Qt, "_QtWidgets", Qt._QtGui) - if hasattr(Qt._QtGui, "QX11Info"): - setattr(Qt, "QtX11Extras", _new_module("QtX11Extras")) - Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info - - Qt.QtCompat.setSectionResizeMode = \ - Qt._QtGui.QHeaderView.setResizeMode - - if hasattr(Qt, "_QtCore"): - Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR - Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR - - _reassign_misplaced_members("PyQt4") - - # QFileDialog QtCompat decorator - def _standardizeQFileDialog(some_function): - """Decorator that makes PyQt4 return conform to other bindings""" - def wrapper(*args, **kwargs): - ret = (some_function(*args, **kwargs)) - - # PyQt4 only returns the selected filename, force it to a - # standard return of the selected filename, and a empty string - # for the selected filter - return ret, '' - - wrapper.__doc__ = some_function.__doc__ - wrapper.__name__ = some_function.__name__ - - return wrapper - - decorators = { - "QFileDialog": { - "getOpenFileName": _standardizeQFileDialog, - "getOpenFileNames": _standardizeQFileDialog, - "getSaveFileName": _standardizeQFileDialog, - } - } - _build_compatibility_members('PyQt4', decorators) - - -def _none(): - """Internal option (used in installer)""" - - Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None}) - - Qt.__binding__ = "None" - Qt.__qt_version__ = "0.0.0" - Qt.__binding_version__ = "0.0.0" - Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None - Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None - - for submodule in _common_members.keys(): - setattr(Qt, submodule, Mock()) - setattr(Qt, "_" + submodule, Mock()) - - -def _log(text): - if QT_VERBOSE: - sys.stdout.write(text + "\n") - - -def _convert(lines): - """Convert compiled .ui file from PySide2 to Qt.py - - Arguments: - lines (list): Each line of of .ui file - - Usage: - >> with open("myui.py") as f: - .. lines = _convert(f.readlines()) - - """ - - def parse(line): - line = line.replace("from PySide2 import", "from Qt import QtCompat,") - line = line.replace("QtWidgets.QApplication.translate", - "QtCompat.translate") - if "QtCore.SIGNAL" in line: - raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 " - "and so Qt.py does not support it: you " - "should avoid defining signals inside " - "your ui files.") - return line - - parsed = list() - for line in lines: - line = parse(line) - parsed.append(line) - - return parsed - - -def _cli(args): - """Qt.py command-line interface""" - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument("--convert", - help="Path to compiled Python module, e.g. my_ui.py") - parser.add_argument("--compile", - help="Accept raw .ui file and compile with native " - "PySide2 compiler.") - parser.add_argument("--stdout", - help="Write to stdout instead of file", - action="store_true") - parser.add_argument("--stdin", - help="Read from stdin instead of file", - action="store_true") - - args = parser.parse_args(args) - - if args.stdout: - raise NotImplementedError("--stdout") - - if args.stdin: - raise NotImplementedError("--stdin") - - if args.compile: - raise NotImplementedError("--compile") - - if args.convert: - sys.stdout.write("#\n" - "# WARNING: --convert is an ALPHA feature.\n#\n" - "# See https://github.com/mottosso/Qt.py/pull/132\n" - "# for details.\n" - "#\n") - - # - # ------> Read - # - with open(args.convert) as f: - lines = _convert(f.readlines()) - - backup = "%s_backup%s" % os.path.splitext(args.convert) - sys.stdout.write("Creating \"%s\"..\n" % backup) - shutil.copy(args.convert, backup) - - # - # <------ Write - # - with open(args.convert, "w") as f: - f.write("".join(lines)) - - sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) - - -def _install(): - # Default order (customise order and content via QT_PREFERRED_BINDING) - default_order = ("PySide2", "PyQt5", "PySide", "PyQt4") - preferred_order = list( - b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b - ) - - order = preferred_order or default_order - - available = { - "PySide2": _pyside2, - "PyQt5": _pyqt5, - "PySide": _pyside, - "PyQt4": _pyqt4, - "None": _none - } - - _log("Order: '%s'" % "', '".join(order)) - - # Allow site-level customization of the available modules. - _apply_site_config() - - found_binding = False - for name in order: - _log("Trying %s" % name) - - try: - available[name]() - found_binding = True - break - - except ImportError as e: - _log("ImportError: %s" % e) - - except KeyError: - _log("ImportError: Preferred binding '%s' not found." % name) - - if not found_binding: - # If not binding were found, throw this error - raise ImportError("No Qt binding were found.") - - # Install individual members - for name, members in _common_members.items(): - try: - their_submodule = getattr(Qt, "_%s" % name) - except AttributeError: - continue - - our_submodule = getattr(Qt, name) - - # Enable import * - __all__.append(name) - - # Enable direct import of submodule, - # e.g. import Qt.QtCore - sys.modules[__name__ + "." + name] = our_submodule - - for member in members: - # Accept that a submodule may miss certain members. - try: - their_member = getattr(their_submodule, member) - except AttributeError: - _log("'%s.%s' was missing." % (name, member)) - continue - - setattr(our_submodule, member, their_member) - - # Enable direct import of QtCompat - sys.modules['Qt.QtCompat'] = Qt.QtCompat - - # Backwards compatibility - if hasattr(Qt.QtCompat, 'loadUi'): - Qt.QtCompat.load_ui = Qt.QtCompat.loadUi - - -_install() - -# Setup Binding Enum states -Qt.IsPySide2 = Qt.__binding__ == 'PySide2' -Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5' -Qt.IsPySide = Qt.__binding__ == 'PySide' -Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4' - -"""Augment QtCompat - -QtCompat contains wrappers and added functionality -to the original bindings, such as the CLI interface -and otherwise incompatible members between bindings, -such as `QHeaderView.setSectionResizeMode`. - -""" - -Qt.QtCompat._cli = _cli -Qt.QtCompat._convert = _convert - -# Enable command-line interface -if __name__ == "__main__": - _cli(sys.argv[1:]) - - -# The MIT License (MIT) -# -# Copyright (c) 2016-2017 Marcus Ottosson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -# -# In PySide(2), loadUi does not exist, so we implement it -# -# `_UiLoader` is adapted from the qtpy project, which was further influenced -# by qt-helpers which was released under a 3-clause BSD license which in turn -# is based on a solution at: -# -# - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 -# -# The License for this code is as follows: -# -# qt-helpers - a common front-end to various Qt modules -# -# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille -# -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the -# distribution. -# * Neither the name of the Glue project nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS -# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# Which itself was based on the solution at -# -# https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 -# -# which was released under the MIT license: -# -# Copyright (c) 2011 Sebastian Wiesner -# Modifications by Charl Botha -# -# Permission is hereby granted, free of charge, to any person obtaining a -# copy of this software and associated documentation files -# (the "Software"),to deal in the Software without restriction, -# including without limitation -# the rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Software, and to permit persons to whom the -# Software is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/animation.py b/pype/tools/pyblish_pype/vendor/qtawesome/animation.py index a9638d74b0..e2a701785a 100644 --- a/pype/tools/pyblish_pype/vendor/qtawesome/animation.py +++ b/pype/tools/pyblish_pype/vendor/qtawesome/animation.py @@ -1,4 +1,4 @@ -from ..Qt import QtCore +from Qt import QtCore class Spin: diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py b/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py index 70f5ec2dec..cd937d7e7f 100644 --- a/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py +++ b/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py @@ -5,8 +5,8 @@ from __future__ import print_function import json import os -from .. import six -from ..Qt import QtCore, QtGui +import six +from Qt import QtCore, QtGui _default_options = { diff --git a/pype/tools/pyblish_pype/vendor/six.py b/pype/tools/pyblish_pype/vendor/six.py deleted file mode 100644 index 190c0239cd..0000000000 --- a/pype/tools/pyblish_pype/vendor/six.py +++ /dev/null @@ -1,868 +0,0 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2015 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from __future__ import absolute_import - -import functools -import itertools -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.10.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 -PY34 = sys.version_info[0:2] >= (3, 4) - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) # Invokes __set__. - try: - # This is a bit ugly, but it avoids running this again by - # removing this descriptor. - delattr(obj.__class__, self.name) - except AttributeError: - pass - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - def __getattr__(self, attr): - _module = self._resolve() - value = getattr(_module, attr) - setattr(self, attr, value) - return value - - -class _LazyModule(types.ModuleType): - - def __init__(self, name): - super(_LazyModule, self).__init__(name) - self.__doc__ = self.__class__.__doc__ - - def __dir__(self): - attrs = ["__doc__", "__name__"] - attrs += [attr.name for attr in self._moved_attributes] - return attrs - - # Subclasses should override this - _moved_attributes = [] - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - -class _SixMetaPathImporter(object): - - """ - A meta path importer to import six.moves and its submodules. - - This class implements a PEP302 finder and loader. It should be compatible - with Python 2.5 and all existing versions of Python3 - """ - - def __init__(self, six_module_name): - self.name = six_module_name - self.known_modules = {} - - def _add_module(self, mod, *fullnames): - for fullname in fullnames: - self.known_modules[self.name + "." + fullname] = mod - - def _get_module(self, fullname): - return self.known_modules[self.name + "." + fullname] - - def find_module(self, fullname, path=None): - if fullname in self.known_modules: - return self - return None - - def __get_module(self, fullname): - try: - return self.known_modules[fullname] - except KeyError: - raise ImportError("This loader does not know module " + fullname) - - def load_module(self, fullname): - try: - # in case of a reload - return sys.modules[fullname] - except KeyError: - pass - mod = self.__get_module(fullname) - if isinstance(mod, MovedModule): - mod = mod._resolve() - else: - mod.__loader__ = self - sys.modules[fullname] = mod - return mod - - def is_package(self, fullname): - """ - Return true, if the named module is a package. - - We need this method to get correct spec objects with - Python 3.4 (see PEP451) - """ - return hasattr(self.__get_module(fullname), "__path__") - - def get_code(self, fullname): - """Return None - - Required, if is_package is implemented""" - self.__get_module(fullname) # eventually raises ImportError - return None - get_source = get_code # same as get_code - -_importer = _SixMetaPathImporter(__name__) - - -class _MovedItems(_LazyModule): - - """Lazy loading of moved objects""" - __path__ = [] # mark as package - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("intern", "__builtin__", "sys"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), - MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections"), - MovedAttribute("UserList", "UserList", "collections"), - MovedAttribute("UserString", "UserString", "collections"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("_thread", "thread", "_thread"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), - MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), - MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), - MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), -] -# Add windows specific modules. -if sys.platform == "win32": - _moved_attributes += [ - MovedModule("winreg", "_winreg"), - ] - -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) - if isinstance(attr, MovedModule): - _importer._add_module(attr, "moves." + attr.name) -del attr - -_MovedItems._moved_attributes = _moved_attributes - -moves = _MovedItems(__name__ + ".moves") -_importer._add_module(moves, "moves") - - -class Module_six_moves_urllib_parse(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_parse""" - - -_urllib_parse_moved_attributes = [ - MovedAttribute("ParseResult", "urlparse", "urllib.parse"), - MovedAttribute("SplitResult", "urlparse", "urllib.parse"), - MovedAttribute("parse_qs", "urlparse", "urllib.parse"), - MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), - MovedAttribute("urldefrag", "urlparse", "urllib.parse"), - MovedAttribute("urljoin", "urlparse", "urllib.parse"), - MovedAttribute("urlparse", "urlparse", "urllib.parse"), - MovedAttribute("urlsplit", "urlparse", "urllib.parse"), - MovedAttribute("urlunparse", "urlparse", "urllib.parse"), - MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), - MovedAttribute("quote", "urllib", "urllib.parse"), - MovedAttribute("quote_plus", "urllib", "urllib.parse"), - MovedAttribute("unquote", "urllib", "urllib.parse"), - MovedAttribute("unquote_plus", "urllib", "urllib.parse"), - MovedAttribute("urlencode", "urllib", "urllib.parse"), - MovedAttribute("splitquery", "urllib", "urllib.parse"), - MovedAttribute("splittag", "urllib", "urllib.parse"), - MovedAttribute("splituser", "urllib", "urllib.parse"), - MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), - MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), - MovedAttribute("uses_params", "urlparse", "urllib.parse"), - MovedAttribute("uses_query", "urlparse", "urllib.parse"), - MovedAttribute("uses_relative", "urlparse", "urllib.parse"), -] -for attr in _urllib_parse_moved_attributes: - setattr(Module_six_moves_urllib_parse, attr.name, attr) -del attr - -Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes - -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), - "moves.urllib_parse", "moves.urllib.parse") - - -class Module_six_moves_urllib_error(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_error""" - - -_urllib_error_moved_attributes = [ - MovedAttribute("URLError", "urllib2", "urllib.error"), - MovedAttribute("HTTPError", "urllib2", "urllib.error"), - MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), -] -for attr in _urllib_error_moved_attributes: - setattr(Module_six_moves_urllib_error, attr.name, attr) -del attr - -Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes - -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), - "moves.urllib_error", "moves.urllib.error") - - -class Module_six_moves_urllib_request(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_request""" - - -_urllib_request_moved_attributes = [ - MovedAttribute("urlopen", "urllib2", "urllib.request"), - MovedAttribute("install_opener", "urllib2", "urllib.request"), - MovedAttribute("build_opener", "urllib2", "urllib.request"), - MovedAttribute("pathname2url", "urllib", "urllib.request"), - MovedAttribute("url2pathname", "urllib", "urllib.request"), - MovedAttribute("getproxies", "urllib", "urllib.request"), - MovedAttribute("Request", "urllib2", "urllib.request"), - MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), - MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), - MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), - MovedAttribute("BaseHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), - MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), - MovedAttribute("FileHandler", "urllib2", "urllib.request"), - MovedAttribute("FTPHandler", "urllib2", "urllib.request"), - MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), - MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), - MovedAttribute("urlretrieve", "urllib", "urllib.request"), - MovedAttribute("urlcleanup", "urllib", "urllib.request"), - MovedAttribute("URLopener", "urllib", "urllib.request"), - MovedAttribute("FancyURLopener", "urllib", "urllib.request"), - MovedAttribute("proxy_bypass", "urllib", "urllib.request"), -] -for attr in _urllib_request_moved_attributes: - setattr(Module_six_moves_urllib_request, attr.name, attr) -del attr - -Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes - -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), - "moves.urllib_request", "moves.urllib.request") - - -class Module_six_moves_urllib_response(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_response""" - - -_urllib_response_moved_attributes = [ - MovedAttribute("addbase", "urllib", "urllib.response"), - MovedAttribute("addclosehook", "urllib", "urllib.response"), - MovedAttribute("addinfo", "urllib", "urllib.response"), - MovedAttribute("addinfourl", "urllib", "urllib.response"), -] -for attr in _urllib_response_moved_attributes: - setattr(Module_six_moves_urllib_response, attr.name, attr) -del attr - -Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes - -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), - "moves.urllib_response", "moves.urllib.response") - - -class Module_six_moves_urllib_robotparser(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_robotparser""" - - -_urllib_robotparser_moved_attributes = [ - MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), -] -for attr in _urllib_robotparser_moved_attributes: - setattr(Module_six_moves_urllib_robotparser, attr.name, attr) -del attr - -Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes - -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), - "moves.urllib_robotparser", "moves.urllib.robotparser") - - -class Module_six_moves_urllib(types.ModuleType): - - """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" - __path__ = [] # mark as package - parse = _importer._get_module("moves.urllib_parse") - error = _importer._get_module("moves.urllib_error") - request = _importer._get_module("moves.urllib_request") - response = _importer._get_module("moves.urllib_response") - robotparser = _importer._get_module("moves.urllib_robotparser") - - def __dir__(self): - return ['parse', 'error', 'request', 'response', 'robotparser'] - -_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), - "moves.urllib") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - def create_unbound_method(func, cls): - return func - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - def create_unbound_method(func, cls): - return types.MethodType(func, None, cls) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -if PY3: - def iterkeys(d, **kw): - return iter(d.keys(**kw)) - - def itervalues(d, **kw): - return iter(d.values(**kw)) - - def iteritems(d, **kw): - return iter(d.items(**kw)) - - def iterlists(d, **kw): - return iter(d.lists(**kw)) - - viewkeys = operator.methodcaller("keys") - - viewvalues = operator.methodcaller("values") - - viewitems = operator.methodcaller("items") -else: - def iterkeys(d, **kw): - return d.iterkeys(**kw) - - def itervalues(d, **kw): - return d.itervalues(**kw) - - def iteritems(d, **kw): - return d.iteritems(**kw) - - def iterlists(d, **kw): - return d.iterlists(**kw) - - viewkeys = operator.methodcaller("viewkeys") - - viewvalues = operator.methodcaller("viewvalues") - - viewitems = operator.methodcaller("viewitems") - -_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") -_add_doc(itervalues, "Return an iterator over the values of a dictionary.") -_add_doc(iteritems, - "Return an iterator over the (key, value) pairs of a dictionary.") -_add_doc(iterlists, - "Return an iterator over the (key, [values]) pairs of a dictionary.") - - -if PY3: - def b(s): - return s.encode("latin-1") - - def u(s): - return s - unichr = chr - import struct - int2byte = struct.Struct(">B").pack - del struct - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO - _assertCountEqual = "assertCountEqual" - if sys.version_info[1] <= 1: - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" - else: - _assertRaisesRegex = "assertRaisesRegex" - _assertRegex = "assertRegex" -else: - def b(s): - return s - # Workaround for standalone backslash - - def u(s): - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - unichr = unichr - int2byte = chr - - def byte2int(bs): - return ord(bs[0]) - - def indexbytes(buf, i): - return ord(buf[i]) - iterbytes = functools.partial(itertools.imap, ord) - import StringIO - StringIO = BytesIO = StringIO.StringIO - _assertCountEqual = "assertItemsEqual" - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -def assertCountEqual(self, *args, **kwargs): - return getattr(self, _assertCountEqual)(*args, **kwargs) - - -def assertRaisesRegex(self, *args, **kwargs): - return getattr(self, _assertRaisesRegex)(*args, **kwargs) - - -def assertRegex(self, *args, **kwargs): - return getattr(self, _assertRegex)(*args, **kwargs) - - -if PY3: - exec_ = getattr(moves.builtins, "exec") - - def reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - -if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - if from_value is None: - raise value - raise value from from_value -""") -elif sys.version_info[:2] > (3, 2): - exec_("""def raise_from(value, from_value): - raise value from from_value -""") -else: - def raise_from(value, from_value): - raise value - - -print_ = getattr(moves.builtins, "print", None) -if print_ is None: - def print_(*args, **kwargs): - """The new-style print function for Python 2.4 and 2.5.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - - def write(data): - if not isinstance(data, basestring): - data = str(data) - # If the file has an encoding, encode unicode with it. - if (isinstance(fp, file) and - isinstance(data, unicode) and - fp.encoding is not None): - errors = getattr(fp, "errors", None) - if errors is None: - errors = "strict" - data = data.encode(fp.encoding, errors) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) -if sys.version_info[:2] < (3, 3): - _print = print_ - - def print_(*args, **kwargs): - fp = kwargs.get("file", sys.stdout) - flush = kwargs.pop("flush", False) - _print(*args, **kwargs) - if flush and fp is not None: - fp.flush() - -_add_doc(reraise, """Reraise an exception.""") - -if sys.version_info[0:2] < (3, 4): - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - def wrapper(f): - f = functools.wraps(wrapped, assigned, updated)(f) - f.__wrapped__ = wrapped - return f - return wrapper -else: - wraps = functools.wraps - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if PY2: - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass - - -# Complete the moves implementation. -# This code is at the end of this module to speed up module loading. -# Turn this module into a package. -__path__ = [] # required for PEP 302 and PEP 451 -__package__ = __name__ # see PEP 366 @ReservedAssignment -if globals().get("__spec__") is not None: - __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable -# Remove other six meta path importers, since they cause problems. This can -# happen if six is removed from sys.modules and then reloaded. (Setuptools does -# this for some reason.) -if sys.meta_path: - for i, importer in enumerate(sys.meta_path): - # Here's some real nastiness: Another "instance" of the six module might - # be floating around. Therefore, we can't use isinstance() to check for - # the six meta path importer, since the other six instance will have - # inserted an importer with different class. - if (type(importer).__name__ == "_SixMetaPathImporter" and - importer.name == __name__): - del sys.meta_path[i] - break - del i, importer -# Finally, add the importer to the meta path import hook. -sys.meta_path.append(_importer) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 86cefd4a55..450f56421c 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -1,6 +1,14 @@ -from .vendor.Qt import QtCore, QtWidgets +from Qt import QtCore, QtWidgets from . import model from .constants import Roles +# Imported when used +widgets = None + + +def _import_widgets(): + global widgets + if widgets is None: + from . import widgets class ArtistView(QtWidgets.QListView): @@ -151,6 +159,8 @@ class TerminalView(QtWidgets.QTreeView): self.clicked.connect(self.item_expand) + _import_widgets() + def event(self, event): if not event.type() == QtCore.QEvent.KeyPress: return super(TerminalView, self).event(event) @@ -190,6 +200,23 @@ class TerminalView(QtWidgets.QTreeView): self.updateGeometry() self.scrollToBottom() + def expand(self, index): + """Wrapper to set widget for expanded index.""" + model = index.model() + row_count = model.rowCount(index) + is_new = False + for child_idx in range(row_count): + child_index = model.index(child_idx, index.column(), index) + widget = self.indexWidget(child_index) + if widget is None: + is_new = True + msg = child_index.data(QtCore.Qt.DisplayRole) + widget = widgets.TerminalDetail(msg) + self.setIndexWidget(child_index, widget) + super(TerminalView, self).expand(index) + if is_new: + self.updateGeometries() + def resizeEvent(self, event): super(self.__class__, self).resizeEvent(event) self.model().layoutChanged.emit() diff --git a/pype/tools/pyblish_pype/widgets.py b/pype/tools/pyblish_pype/widgets.py index 3a09249a86..880d4755ad 100644 --- a/pype/tools/pyblish_pype/widgets.py +++ b/pype/tools/pyblish_pype/widgets.py @@ -1,5 +1,5 @@ import sys -from .vendor.Qt import QtCore, QtWidgets, QtGui +from Qt import QtCore, QtWidgets, QtGui from . import model, delegate, view, awesome from .constants import PluginStates, InstanceStates, Roles @@ -321,11 +321,6 @@ class PerspectiveWidget(QtWidgets.QWidget): data = {"records": records} self.terminal_model.reset() self.terminal_model.update_with_result(data) - while not self.terminal_model.items_to_set_widget.empty(): - item = self.terminal_model.items_to_set_widget.get() - widget = TerminalDetail(item.data(QtCore.Qt.DisplayRole)) - index = self.terminal_proxy.mapFromSource(item.index()) - self.terminal_view.setIndexWidget(index, widget) self.records.button_toggle_text.setText( "{} ({})".format(self.l_rec, len_records) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 84003a88aa..3c7808496c 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -44,7 +44,7 @@ from functools import partial from . import delegate, model, settings, util, view, widgets from .awesome import tags as awesome -from .vendor.Qt import QtCore, QtGui, QtWidgets +from Qt import QtCore, QtGui, QtWidgets from .constants import ( PluginStates, PluginActionStates, InstanceStates, GroupStates, Roles ) @@ -54,6 +54,7 @@ class Window(QtWidgets.QDialog): def __init__(self, controller, parent=None): super(Window, self).__init__(parent=parent) + self._suspend_logs = False # Use plastique style for specific ocations # TODO set style name via environment variable low_keys = { @@ -95,6 +96,18 @@ class Window(QtWidgets.QDialog): header_tab_terminal = QtWidgets.QRadioButton(header_tab_widget) header_spacer = QtWidgets.QWidget(header_tab_widget) + button_suspend_logs_widget = QtWidgets.QWidget() + button_suspend_logs_widget_layout = QtWidgets.QHBoxLayout( + button_suspend_logs_widget + ) + button_suspend_logs_widget_layout.setContentsMargins(0, 10, 0, 10) + button_suspend_logs = QtWidgets.QPushButton(header_widget) + button_suspend_logs.setFixedWidth(7) + button_suspend_logs.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, + QtWidgets.QSizePolicy.Expanding + ) + button_suspend_logs_widget_layout.addWidget(button_suspend_logs) header_aditional_btns = QtWidgets.QWidget(header_tab_widget) aditional_btns_layout = QtWidgets.QHBoxLayout(header_aditional_btns) @@ -109,9 +122,11 @@ class Window(QtWidgets.QDialog): layout_tab.addWidget(header_tab_artist, 0) layout_tab.addWidget(header_tab_overview, 0) layout_tab.addWidget(header_tab_terminal, 0) + layout_tab.addWidget(button_suspend_logs_widget, 0) + # Compress items to the left layout_tab.addWidget(header_spacer, 1) - layout_tab.addWidget(header_aditional_btns, 1) + layout_tab.addWidget(header_aditional_btns, 0) layout = QtWidgets.QHBoxLayout(header_widget) layout.setContentsMargins(0, 0, 0, 0) @@ -226,6 +241,10 @@ class Window(QtWidgets.QDialog): footer_info = QtWidgets.QLabel(footer_widget) footer_spacer = QtWidgets.QWidget(footer_widget) + + footer_button_stop = QtWidgets.QPushButton( + awesome["stop"], footer_widget + ) footer_button_reset = QtWidgets.QPushButton( awesome["refresh"], footer_widget ) @@ -235,14 +254,12 @@ class Window(QtWidgets.QDialog): footer_button_play = QtWidgets.QPushButton( awesome["play"], footer_widget ) - footer_button_stop = QtWidgets.QPushButton( - awesome["stop"], footer_widget - ) layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(5, 5, 5, 5) layout.addWidget(footer_info, 0) layout.addWidget(footer_spacer, 1) + layout.addWidget(footer_button_stop, 0) layout.addWidget(footer_button_reset, 0) layout.addWidget(footer_button_validate, 0) @@ -342,10 +359,11 @@ class Window(QtWidgets.QDialog): "TerminalView": terminal_view, # Buttons - "Play": footer_button_play, - "Validate": footer_button_validate, - "Reset": footer_button_reset, + "SuspendLogsBtn": button_suspend_logs, "Stop": footer_button_stop, + "Reset": footer_button_reset, + "Validate": footer_button_validate, + "Play": footer_button_play, # Misc "HeaderSpacer": header_spacer, @@ -370,10 +388,11 @@ class Window(QtWidgets.QDialog): overview_page, terminal_page, footer_widget, - footer_button_play, - footer_button_validate, + button_suspend_logs, footer_button_stop, footer_button_reset, + footer_button_validate, + footer_button_play, footer_spacer, closing_placeholder ): @@ -415,10 +434,11 @@ class Window(QtWidgets.QDialog): QtCore.Qt.DirectConnection ) - artist_view.toggled.connect(self.on_item_toggled) - overview_instance_view.toggled.connect(self.on_item_toggled) - overview_plugin_view.toggled.connect(self.on_item_toggled) + artist_view.toggled.connect(self.on_instance_toggle) + overview_instance_view.toggled.connect(self.on_instance_toggle) + overview_plugin_view.toggled.connect(self.on_plugin_toggle) + button_suspend_logs.clicked.connect(self.on_suspend_clicked) footer_button_stop.clicked.connect(self.on_stop_clicked) footer_button_reset.clicked.connect(self.on_reset_clicked) footer_button_validate.clicked.connect(self.on_validate_clicked) @@ -442,10 +462,11 @@ class Window(QtWidgets.QDialog): self.terminal_filters_widget = terminal_filters_widget self.footer_widget = footer_widget + self.button_suspend_logs = button_suspend_logs + self.footer_button_stop = footer_button_stop self.footer_button_reset = footer_button_reset self.footer_button_validate = footer_button_validate self.footer_button_play = footer_button_play - self.footer_button_stop = footer_button_stop self.overview_instance_view = overview_instance_view self.overview_plugin_view = overview_plugin_view @@ -537,7 +558,29 @@ class Window(QtWidgets.QDialog): ): instance_item.setData(enable_value, Roles.IsEnabledRole) - def on_item_toggled(self, index, state=None): + def on_instance_toggle(self, index, state=None): + """An item is requesting to be toggled""" + if not index.data(Roles.IsOptionalRole): + return self.info("This item is mandatory") + + if self.controller.collect_state != 1: + return self.info("Cannot toggle") + + current_state = index.data(QtCore.Qt.CheckStateRole) + if state is None: + state = not current_state + + instance_id = index.data(Roles.ObjectIdRole) + instance_item = self.instance_model.instance_items[instance_id] + instance_item.setData(state, QtCore.Qt.CheckStateRole) + + self.controller.instance_toggled.emit( + instance_item.instance, current_state, state + ) + + self.update_compatibility() + + def on_plugin_toggle(self, index, state=None): """An item is requesting to be toggled""" if not index.data(Roles.IsOptionalRole): return self.info("This item is mandatory") @@ -548,7 +591,10 @@ class Window(QtWidgets.QDialog): if state is None: state = not index.data(QtCore.Qt.CheckStateRole) - index.model().setData(index, state, QtCore.Qt.CheckStateRole) + plugin_id = index.data(Roles.ObjectIdRole) + plugin_item = self.plugin_model.plugin_items[plugin_id] + plugin_item.setData(state, QtCore.Qt.CheckStateRole) + self.update_compatibility() def on_tab_changed(self, target): @@ -587,6 +633,13 @@ class Window(QtWidgets.QDialog): self.footer_button_play.setEnabled(False) self.footer_button_stop.setEnabled(False) + def on_suspend_clicked(self): + self._suspend_logs = not self._suspend_logs + if self.state["current_page"] == "terminal": + self.on_tab_changed("overview") + + self.tabs["terminal"].setVisible(not self._suspend_logs) + def on_comment_entered(self): """The user has typed a comment.""" self.controller.context.data["comment"] = self.comment_box.text() @@ -701,14 +754,14 @@ class Window(QtWidgets.QDialog): self.on_tab_changed(self.state["current_page"]) self.update_compatibility() - self.footer_button_validate.setEnabled(True) - self.footer_button_reset.setEnabled(True) - self.footer_button_stop.setEnabled(False) - self.footer_button_play.setEnabled(True) - self.footer_button_play.setFocus() + self.button_suspend_logs.setEnabled(False) + + self.footer_button_validate.setEnabled(False) + self.footer_button_reset.setEnabled(False) + self.footer_button_stop.setEnabled(True) + self.footer_button_play.setEnabled(False) def on_passed_group(self, order): - for group_item in self.instance_model.group_items.values(): if self.overview_instance_view.isExpanded(group_item.index()): continue @@ -740,16 +793,28 @@ class Window(QtWidgets.QDialog): def on_was_stopped(self): errored = self.controller.errored - self.footer_button_play.setEnabled(not errored) - self.footer_button_validate.setEnabled( - not errored and not self.controller.validated - ) + if self.controller.collect_state == 0: + self.footer_button_play.setEnabled(False) + self.footer_button_validate.setEnabled(False) + else: + self.footer_button_play.setEnabled(not errored) + self.footer_button_validate.setEnabled( + not errored and not self.controller.validated + ) + self.footer_button_play.setFocus() + self.footer_button_reset.setEnabled(True) self.footer_button_stop.setEnabled(False) if errored: self.footer_widget.setProperty("success", 0) self.footer_widget.style().polish(self.footer_widget) + suspend_log_bool = ( + self.controller.collect_state == 1 + and not self.controller.stopped + ) + self.button_suspend_logs.setEnabled(suspend_log_bool) + def on_was_skipped(self, plugin): plugin_item = self.plugin_model.plugin_items[plugin.id] plugin_item.setData( @@ -809,17 +874,15 @@ class Window(QtWidgets.QDialog): if self.tabs["artist"].isChecked(): self.tabs["overview"].toggle() - result["records"] = self.terminal_model.prepare_records(result) + result["records"] = self.terminal_model.prepare_records( + result, + self._suspend_logs + ) plugin_item = self.plugin_model.update_with_result(result) instance_item = self.instance_model.update_with_result(result) self.terminal_model.update_with_result(result) - while not self.terminal_model.items_to_set_widget.empty(): - item = self.terminal_model.items_to_set_widget.get() - widget = widgets.TerminalDetail(item.data(QtCore.Qt.DisplayRole)) - index = self.terminal_proxy.mapFromSource(item.index()) - self.terminal_view.setIndexWidget(index, widget) self.update_compatibility() @@ -872,16 +935,19 @@ class Window(QtWidgets.QDialog): self.footer_button_validate.setEnabled(False) self.footer_button_play.setEnabled(False) + self.button_suspend_logs.setEnabled(False) + util.defer(5, self.controller.validate) def publish(self): self.info(self.tr("Preparing publish..")) - self.footer_button_stop.setEnabled(True) self.footer_button_reset.setEnabled(False) self.footer_button_validate.setEnabled(False) self.footer_button_play.setEnabled(False) + self.button_suspend_logs.setEnabled(False) + util.defer(5, self.controller.publish) def act(self, plugin_item, action): @@ -913,29 +979,24 @@ class Window(QtWidgets.QDialog): plugin_item = self.plugin_model.plugin_items[result["plugin"].id] action_state = plugin_item.data(Roles.PluginActionProgressRole) action_state |= PluginActionStates.HasFinished + result["records"] = self.terminal_model.prepare_records( + result, + self._suspend_logs + ) - error = result.get("error") - if error: - records = result.get("records") or [] + if result.get("error"): action_state |= PluginActionStates.HasFailed - fname, line_no, func, exc = error.traceback - - records.append({ - "label": str(error), - "type": "error", - "filename": str(fname), - "lineno": str(line_no), - "func": str(func), - "traceback": error.formatted_traceback - }) - - result["records"] = records plugin_item.setData(action_state, Roles.PluginActionProgressRole) - self.plugin_model.update_with_result(result) - self.instance_model.update_with_result(result) self.terminal_model.update_with_result(result) + plugin_item = self.plugin_model.update_with_result(result) + instance_item = self.instance_model.update_with_result(result) + + if self.perspective_widget.isVisible(): + self.perspective_widget.update_context( + plugin_item, instance_item + ) def closeEvent(self, event): """Perform post-flight checks before closing diff --git a/pype/version.py b/pype/version.py index 334087f851..1c622223ba 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.9.1" +__version__ = "2.10.0" diff --git a/res/app_icons/celaction_local.png b/res/app_icons/celaction_local.png new file mode 100644 index 0000000000..3a8abe6dbc Binary files /dev/null and b/res/app_icons/celaction_local.png differ diff --git a/res/app_icons/celaction_remotel.png b/res/app_icons/celaction_remotel.png new file mode 100644 index 0000000000..320e8173eb Binary files /dev/null and b/res/app_icons/celaction_remotel.png differ diff --git a/schema/session-2.0.json b/schema/session-2.0.json index d37f2ac822..7ad2c63bcf 100644 --- a/schema/session-2.0.json +++ b/schema/session-2.0.json @@ -56,13 +56,6 @@ "pattern": "^\\w*$", "example": "maya2016" }, - "AVALON_MONGO": { - "description": "Address to the asset database", - "type": "string", - "pattern": "^mongodb://[\\w/@:.]*$", - "example": "mongodb://localhost:27017", - "default": "mongodb://localhost:27017" - }, "AVALON_DB": { "description": "Name of database", "type": "string",