diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 7cd2193086..777a6ffbc9 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -5,6 +5,7 @@ import maya.mel as mel import six import sys +from openpype.lib import Logger from openpype.api import ( get_project_settings, get_current_project_settings @@ -38,6 +39,8 @@ class RenderSettings(object): "underscore": "_" } + log = Logger.get_logger("RenderSettings") + @classmethod def get_image_prefix_attr(cls, renderer): return cls._image_prefix_nodes[renderer] @@ -133,20 +136,7 @@ class RenderSettings(object): cmds.setAttr( "defaultArnoldDriver.mergeAOVs", multi_exr) - # Passes additional options in from the schema as a list - # but converts it to a dictionary because ftrack doesn't - # allow fullstops in custom attributes. Then checks for - # type of MtoA attribute passed to adjust the `setAttr` - # command accordingly. self._additional_attribs_setter(additional_options) - for item in additional_options: - attribute, value = item - if (cmds.getAttr(str(attribute), type=True)) == "long": - cmds.setAttr(str(attribute), int(value)) - elif (cmds.getAttr(str(attribute), type=True)) == "bool": - cmds.setAttr(str(attribute), int(value), type = "Boolean") # noqa - elif (cmds.getAttr(str(attribute), type=True)) == "string": - cmds.setAttr(str(attribute), str(value), type = "string") # noqa reset_frame_range() def _set_redshift_settings(self, width, height): @@ -230,12 +220,20 @@ class RenderSettings(object): cmds.setAttr("defaultRenderGlobals.extensionPadding", 4) def _additional_attribs_setter(self, additional_attribs): - print(additional_attribs) for item in additional_attribs: attribute, value = item - if (cmds.getAttr(str(attribute), type=True)) == "long": - cmds.setAttr(str(attribute), int(value)) - elif (cmds.getAttr(str(attribute), type=True)) == "bool": - cmds.setAttr(str(attribute), int(value)) # noqa - elif (cmds.getAttr(str(attribute), type=True)) == "string": - cmds.setAttr(str(attribute), str(value), type = "string") # noqa + attribute = str(attribute) # ensure str conversion from settings + attribute_type = cmds.getAttr(attribute, type=True) + if attribute_type in {"long", "bool"}: + cmds.setAttr(attribute, int(value)) + elif attribute_type == "string": + cmds.setAttr(attribute, str(value), type="string") + elif attribute_type in {"double", "doubleAngle", "doubleLinear"}: + cmds.setAttr(attribute, float(value)) + else: + self.log.error( + "Attribute {attribute} can not be set due to unsupported " + "type: {attribute_type}".format( + attribute=attribute, + attribute_type=attribute_type) + ) diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py index 91b0da75c6..403b4ee6bc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_look.py +++ b/openpype/hosts/maya/plugins/publish/extract_look.py @@ -13,7 +13,7 @@ from maya import cmds # noqa import pyblish.api -from openpype.lib import source_hash +from openpype.lib import source_hash, run_subprocess from openpype.pipeline import legacy_io, publish from openpype.hosts.maya.api import lib @@ -68,7 +68,7 @@ def find_paths_by_hash(texture_hash): return legacy_io.distinct(key, {"type": "version"}) -def maketx(source, destination, *args): +def maketx(source, destination, args, logger): """Make `.tx` using `maketx` with some default settings. The settings are based on default as used in Arnold's @@ -79,7 +79,8 @@ def maketx(source, destination, *args): Args: source (str): Path to source file. destination (str): Writing destination path. - *args: Additional arguments for `maketx`. + args (list): Additional arguments for `maketx`. + logger (logging.Logger): Logger to log messages to. Returns: str: Output of `maketx` command. @@ -94,7 +95,7 @@ def maketx(source, destination, *args): "OIIO tool not found in {}".format(maketx_path)) raise AssertionError("OIIO tool not found") - cmd = [ + subprocess_args = [ maketx_path, "-v", # verbose "-u", # update mode @@ -103,27 +104,20 @@ def maketx(source, destination, *args): "--checknan", # use oiio-optimized settings for tile-size, planarconfig, metadata "--oiio", - "--filter lanczos3", - escape_space(source) + "--filter", "lanczos3", + source ] - cmd.extend(args) - cmd.extend(["-o", escape_space(destination)]) + subprocess_args.extend(args) + subprocess_args.extend(["-o", destination]) - cmd = " ".join(cmd) + cmd = " ".join(subprocess_args) + logger.debug(cmd) - CREATE_NO_WINDOW = 0x08000000 # noqa - kwargs = dict(args=cmd, stderr=subprocess.STDOUT) - - if sys.platform == "win32": - kwargs["creationflags"] = CREATE_NO_WINDOW try: - out = subprocess.check_output(**kwargs) - except subprocess.CalledProcessError as exc: - print(exc) - import traceback - - traceback.print_exc() + out = run_subprocess(subprocess_args) + except Exception: + logger.error("Maketx converion failed", exc_info=True) raise return out @@ -524,15 +518,17 @@ class ExtractLook(publish.Extractor): if do_maketx and ext != ".tx": # Produce .tx file in staging if source file is not .tx converted = os.path.join(staging, "resources", fname + ".tx") - + additional_args = [ + "--sattrib", + "sourceHash", + texture_hash + ] if linearize: self.log.info("tx: converting sRGB -> linear") - colorconvert = "--colorconvert sRGB linear" - else: - colorconvert = "" + additional_args.extend(["--colorconvert", "sRGB", "linear"]) config_path = get_ocio_config_path("nuke-default") - color_config = "--colorconfig {0}".format(config_path) + additional_args.extend(["--colorconfig", config_path]) # Ensure folder exists if not os.path.exists(os.path.dirname(converted)): os.makedirs(os.path.dirname(converted)) @@ -541,12 +537,8 @@ class ExtractLook(publish.Extractor): maketx( filepath, converted, - # Include `source-hash` as string metadata - "--sattrib", - "sourceHash", - escape_space(texture_hash), - colorconvert, - color_config + additional_args, + self.log ) return converted, COPY, texture_hash diff --git a/openpype/hosts/photoshop/plugins/publish/collect_published_version.py b/openpype/hosts/photoshop/plugins/publish/collect_published_version.py new file mode 100644 index 0000000000..2502689e4b --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_published_version.py @@ -0,0 +1,55 @@ +"""Collects published version of workfile and increments it. + +For synchronization of published image and workfile version it is required +to store workfile version from workfile file name in context.data["version"]. +In remote publishing this name is unreliable (artist might not follow naming +convention etc.), last published workfile version for particular workfile +subset is used instead. + +This plugin runs only in remote publishing (eg. Webpublisher). + +Requires: + context.data["assetEntity"] + +Provides: + context["version"] - incremented latest published workfile version +""" + +import pyblish.api + +from openpype.client import get_last_version_by_subset_name + + +class CollectPublishedVersion(pyblish.api.ContextPlugin): + """Collects published version of workfile and increments it.""" + + order = pyblish.api.CollectorOrder + 0.190 + label = "Collect published version" + hosts = ["photoshop"] + targets = ["remotepublish"] + + def process(self, context): + workfile_subset_name = None + for instance in context: + if instance.data["family"] == "workfile": + workfile_subset_name = instance.data["subset"] + break + + if not workfile_subset_name: + self.log.warning("No workfile instance found, " + "synchronization of version will not work.") + return + + project_name = context.data["projectName"] + asset_doc = context.data["assetEntity"] + asset_id = asset_doc["_id"] + + version_doc = get_last_version_by_subset_name(project_name, + workfile_subset_name, + asset_id) + version_int = 1 + if version_doc: + version_int += int(version_doc["name"]) + + self.log.debug(f"Setting {version_int} to context.") + context.data["version"] = version_int diff --git a/openpype/hosts/photoshop/plugins/publish/collect_version.py b/openpype/hosts/photoshop/plugins/publish/collect_version.py new file mode 100644 index 0000000000..aff9f13bfb --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/collect_version.py @@ -0,0 +1,24 @@ +import pyblish.api + + +class CollectVersion(pyblish.api.InstancePlugin): + """Collect version for publishable instances. + + Used to synchronize version from workfile to all publishable instances: + - image (manually created or color coded) + - review + + Dev comment: + Explicit collector created to control this from single place and not from + 3 different. + """ + order = pyblish.api.CollectorOrder + 0.200 + label = 'Collect Version' + + hosts = ["photoshop"] + families = ["image", "review"] + + def process(self, instance): + workfile_version = instance.context.data["version"] + self.log.debug(f"Applying version {workfile_version}") + instance.data["version"] = workfile_version diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 96d768e1c1..c0760a5471 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -577,8 +577,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): return except OSError as exc: - # re-raise exception if different than cross drive path - if exc.errno != errno.EXDEV: + # re-raise exception if different than + # EXDEV - cross drive path + # EINVAL - wrong format, must be NTFS + self.log.debug("Hardlink failed with errno:'{}'".format(exc.errno)) + if exc.errno not in [errno.EXDEV, errno.EINVAL]: raise shutil.copy(src_path, dst_path) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 85561495fd..f65d969c53 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -187,7 +187,7 @@ class PypeCommands: (to choose validator for example) """ - from openpype.hosts.webpublisher.cli_functions import ( + from openpype.hosts.webpublisher.publish_functions import ( cli_publish_from_app ) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 8643297f02..76ef0a7338 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -49,7 +49,7 @@ "vray_renderer": { "image_prefix": "maya///", "engine": "1", - "image_format": "png", + "image_format": "exr", "aov_list": [], "additional_options": [] }, @@ -57,7 +57,7 @@ "image_prefix": "maya///", "primary_gi_engine": "0", "secondary_gi_engine": "0", - "image_format": "iff", + "image_format": "exr", "multilayer_exr": true, "force_combine": true, "aov_list": [], diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index 552c2c9cad..9d74df7cd5 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -15,6 +15,9 @@ "CollectInstances": { "flatten_subset_template": "" }, + "CollectVersion": { + "enabled": false + }, "ValidateContainers": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json index 7aa49c99a4..e63e25d2c2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -131,6 +131,23 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectVersion", + "label": "Collect Version", + "children": [ + { + "type": "label", + "label": "Synchronize version for image and review instances by workfile version." + }, + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", diff --git a/openpype/tools/traypublisher/window.py b/openpype/tools/traypublisher/window.py index cc33287091..930c27ca9c 100644 --- a/openpype/tools/traypublisher/window.py +++ b/openpype/tools/traypublisher/window.py @@ -7,6 +7,7 @@ publishing plugins. """ from Qt import QtWidgets, QtCore +import qtawesome from openpype.pipeline import ( install_host, @@ -20,6 +21,27 @@ from openpype.tools.utils.models import ( ProjectSortFilterProxy ) +from openpype.tools.utils import PlaceholderLineEdit +import appdirs +from openpype.lib import JSONSettingRegistry + + +class TrayPublisherRegistry(JSONSettingRegistry): + """Class handling OpenPype general settings registry. + + Attributes: + vendor (str): Name used for path construction. + product (str): Additional name used for path construction. + + """ + + def __init__(self): + self.vendor = "pypeclub" + self.product = "openpype" + name = "tray_publisher" + path = appdirs.user_data_dir(self.product, self.vendor) + super(TrayPublisherRegistry, self).__init__(name, path) + class StandaloneOverlayWidget(QtWidgets.QFrame): project_selected = QtCore.Signal(str) @@ -43,6 +65,7 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): projects_model = ProjectModel(dbcon) projects_proxy = ProjectSortFilterProxy() projects_proxy.setSourceModel(projects_model) + projects_proxy.setFilterKeyColumn(0) projects_view = QtWidgets.QListView(content_widget) projects_view.setObjectName("ChooseProjectView") @@ -59,10 +82,17 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): btns_layout.addWidget(cancel_btn, 0) btns_layout.addWidget(confirm_btn, 0) + txt_filter = PlaceholderLineEdit(content_widget) + txt_filter.setPlaceholderText("Quick filter projects..") + txt_filter.setClearButtonEnabled(True) + txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"), + QtWidgets.QLineEdit.LeadingPosition) + content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) content_layout.setSpacing(20) content_layout.addWidget(header_label, 0) + content_layout.addWidget(txt_filter, 0) content_layout.addWidget(projects_view, 1) content_layout.addLayout(btns_layout, 0) @@ -79,17 +109,40 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): projects_view.doubleClicked.connect(self._on_double_click) confirm_btn.clicked.connect(self._on_confirm_click) cancel_btn.clicked.connect(self._on_cancel_click) + txt_filter.textChanged.connect(self._on_text_changed) self._projects_view = projects_view self._projects_model = projects_model + self._projects_proxy = projects_proxy self._cancel_btn = cancel_btn self._confirm_btn = confirm_btn + self._txt_filter = txt_filter self._publisher_window = publisher_window self._project_name = None def showEvent(self, event): self._projects_model.refresh() + # Sort projects after refresh + self._projects_proxy.sort(0) + + setting_registry = TrayPublisherRegistry() + try: + project_name = setting_registry.get_item("project_name") + except ValueError: + project_name = None + + if project_name: + index = None + src_index = self._projects_model.find_project(project_name) + if src_index is not None: + index = self._projects_proxy.mapFromSource(src_index) + if index: + mode = ( + QtCore.QItemSelectionModel.Select + | QtCore.QItemSelectionModel.Rows) + self._projects_view.selectionModel().select(index, mode) + self._cancel_btn.setVisible(self._project_name is not None) super(StandaloneOverlayWidget, self).showEvent(event) @@ -102,6 +155,10 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): def _on_cancel_click(self): self._set_project(self._project_name) + def _on_text_changed(self): + self._projects_proxy.setFilterRegularExpression( + self._txt_filter.text()) + def set_selected_project(self): index = self._projects_view.currentIndex() @@ -119,6 +176,9 @@ class StandaloneOverlayWidget(QtWidgets.QFrame): self.setVisible(False) self.project_selected.emit(project_name) + setting_registry = TrayPublisherRegistry() + setting_registry.set_item("project_name", project_name) + class TrayPublishWindow(PublisherWindow): def __init__(self, *args, **kwargs): diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index 1faccef4dd..2e93d94d7e 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -330,11 +330,26 @@ class ProjectModel(QtGui.QStandardItemModel): if new_items: root_item.appendRows(new_items) + def find_project(self, project_name): + """ + Get index of 'project_name' value. + + Args: + project_name (str): + Returns: + (QModelIndex) + """ + val = self._items_by_name.get(project_name) + if val: + return self.indexFromItem(val) + class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): def __init__(self, *args, **kwargs): super(ProjectSortFilterProxy, self).__init__(*args, **kwargs) self._filter_enabled = True + # Disable case sensitivity + self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) def lessThan(self, left_index, right_index): if left_index.data(PROJECT_NAME_ROLE) is None: @@ -356,10 +371,14 @@ class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): def filterAcceptsRow(self, source_row, source_parent): index = self.sourceModel().index(source_row, 0, source_parent) + string_pattern = self.filterRegularExpression().pattern() if self._filter_enabled: result = self._custom_index_filter(index) if result is not None: - return result + project_name = index.data(PROJECT_NAME_ROLE) + if project_name is None: + return result + return string_pattern.lower() in project_name.lower() return super(ProjectSortFilterProxy, self).filterAcceptsRow( source_row, source_parent diff --git a/tools/get_python_packages_info.py b/tools/get_python_packages_info.py new file mode 100644 index 0000000000..b4952840e6 --- /dev/null +++ b/tools/get_python_packages_info.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +"""Get version and license information on used Python packages. + +This is getting over all packages installed with Poetry and printing out +their name, version and available license information from PyPi in Markdown +table format. + +Usage: + ./.poetry/bin/poetry run python ./tools/get_python_packages_info.py + +""" + +import toml +import requests + + +packages = [] + +# define column headers +package_header = "Package" +version_header = "Version" +license_header = "License" + +name_col_width = len(package_header) +version_col_width = len(version_header) +license_col_width = len(license_header) + +# read lock file to get packages +with open("poetry.lock", "r") as fb: + lock_content = toml.load(fb) + + for package in lock_content["package"]: + # query pypi for license information + url = f"https://pypi.org/pypi/{package['name']}/json" + response = requests.get( + f"https://pypi.org/pypi/{package['name']}/json") + package_data = response.json() + version = package.get("version") or "N/A" + try: + package_license = package_data["info"].get("license") or "N/A" + except KeyError: + package_license = "N/A" + + if len(package_license) > 64: + package_license = f"{package_license[:32]}..." + packages.append( + ( + package["name"], + version, + package_license + ) + ) + + # update column width based on max string length + if len(package["name"]) > name_col_width: + name_col_width = len(package["name"]) + if len(version) > version_col_width: + version_col_width = len(version) + if len(package_license) > license_col_width: + license_col_width = len(package_license) + +# pad columns +name_col_width += 2 +version_col_width += 2 +license_col_width += 2 + +# print table header +print((f"|{package_header.center(name_col_width)}" + f"|{version_header.center(version_col_width)}" + f"|{license_header.center(license_col_width)}|")) + +print( + "|" + ("-" * len(package_header.center(name_col_width))) + + "|" + ("-" * len(version_header.center(version_col_width))) + + "|" + ("-" * len(license_header.center(license_col_width))) + "|") + +# print rest of the table +for package in packages: + print(( + f"|{package[0].center(name_col_width)}" + f"|{package[1].center(version_col_width)}" + f"|{package[2].center(license_col_width)}|" + ))