From f8c29e9130598b6484ce2ffb3a3759df4530492e Mon Sep 17 00:00:00 2001 From: kalisp Date: Mon, 24 May 2021 14:17:54 +0000 Subject: [PATCH 001/359] Create draft PR for #170 From 2a65671997095cd3f38cf4b157f5cd047c384bbb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Jun 2021 17:42:59 +0200 Subject: [PATCH 002/359] DL expected files validation - added injection of dependency ids for possible validation --- .../custom/plugins/GlobalJobPreLoad.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py index 5e64605271..41df9d4dc9 100644 --- a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py +++ b/vendor/deadline/custom/plugins/GlobalJobPreLoad.py @@ -9,6 +9,10 @@ from Deadline.Scripting import RepositoryUtils, FileUtils def inject_openpype_environment(deadlinePlugin): + """ Pull env vars from OpenPype and push them to rendering process. + + Used for correct paths, configuration from OpenPype etc. + """ job = deadlinePlugin.GetJob() job = RepositoryUtils.GetJob(job.JobId, True) # invalidates cache @@ -73,6 +77,21 @@ def inject_openpype_environment(deadlinePlugin): raise +def inject_render_job_id(deadlinePlugin): + """Inject dependency ids to publish process as env var for validation.""" + print("inject_render_job_id start") + job = deadlinePlugin.GetJob() + job = RepositoryUtils.GetJob(job.JobId, True) # invalidates cache + + dependency_ids = job.JobDependencyIDs + print("dependency_ids {}".format(dependency_ids)) + render_job_ids = ",".join(dependency_ids) + + deadlinePlugin.SetProcessEnvironmentVariable("RENDER_JOB_IDS", + render_job_ids) + print("inject_render_job_id end") + + def pype_command_line(executable, arguments, workingDirectory): """Remap paths in comand line argument string. @@ -156,8 +175,7 @@ def __main__(deadlinePlugin): "render and publish.") if openpype_publish_job == '1': - print("Publish job, skipping inject.") - return + inject_render_job_id(deadlinePlugin) elif openpype_render_job == '1': inject_openpype_environment(deadlinePlugin) else: From 5ca1eb7a4f4ae28dc4f7e1292df30b0e0b6fe4e1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Jun 2021 17:44:29 +0200 Subject: [PATCH 003/359] DL expected files validation - added 'targets' to 'publish' command for more precise plugin selections Validator for expected files should run only during publishing on farm, targets=deadline handles that --- openpype/cli.py | 6 ++++-- openpype/pype_commands.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index df38c74a21..71f689f159 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -132,7 +132,9 @@ def extractenvironments(output_json_path, project, asset, task, app): @main.command() @click.argument("paths", nargs=-1) @click.option("-d", "--debug", is_flag=True, help="Print debug messages") -def publish(debug, paths): +@click.option("-t", "--targets", help="Targets module", default=None, + multiple=True) +def publish(debug, paths, targets): """Start CLI publishing. Publish collects json from paths provided as an argument. @@ -140,7 +142,7 @@ def publish(debug, paths): """ if debug: os.environ['OPENPYPE_DEBUG'] = '3' - PypeCommands.publish(list(paths)) + PypeCommands.publish(list(paths), targets) @main.command() diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 326ca8349a..349c096e2e 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -43,16 +43,18 @@ class PypeCommands: standalonepublish.main() @staticmethod - def publish(paths): + def publish(paths, targets=None): """Start headless publishing. Publish use json from passed paths argument. Args: paths (list): Paths to jsons. + targets (string): What module should be targeted + (to choose validator for example) Raises: - RuntimeError: When there is no pathto process. + RuntimeError: When there is no path to process. """ if not any(paths): raise RuntimeError("No publish paths specified") @@ -79,6 +81,10 @@ class PypeCommands: pyblish.api.register_target("filesequence") pyblish.api.register_host("shell") + if targets: + for target in targets: + pyblish.api.register_target(target) + os.environ["OPENPYPE_PUBLISH_DATA"] = os.pathsep.join(paths) log.info("Running publish ...") From 1c5eda893c52da3fa3e3b375e22ab271ee70271a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Jun 2021 17:45:24 +0200 Subject: [PATCH 004/359] DL expected files validation - extracted post and get method as they both should be used on more places --- openpype/lib/abstract_submit_deadline.py | 82 +++++++++++++----------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/openpype/lib/abstract_submit_deadline.py b/openpype/lib/abstract_submit_deadline.py index 12014ddfb5..4a052a4ee2 100644 --- a/openpype/lib/abstract_submit_deadline.py +++ b/openpype/lib/abstract_submit_deadline.py @@ -18,6 +18,48 @@ import pyblish.api from .abstract_metaplugins import AbstractMetaInstancePlugin +def requests_post(*args, **kwargs): + """Wrap request post method. + + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line + of defense SSL is providing and it is not recommended. + + """ + if 'verify' not in kwargs: + kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", + True) else True # noqa + # add 10sec timeout before bailing out + kwargs['timeout'] = 10 + return requests.post(*args, **kwargs) + + +def requests_get(*args, **kwargs): + """Wrap request get method. + + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line + of defense SSL is providing and it is not recommended. + + """ + if 'verify' not in kwargs: + kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", + True) else True # noqa + # add 10sec timeout before bailing out + kwargs['timeout'] = 10 + return requests.get(*args, **kwargs) + + @attr.s class DeadlineJobInfo(object): """Mapping of all Deadline *JobInfo* attributes. @@ -579,7 +621,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): """ url = "{}/api/jobs".format(self._deadline_url) - response = self._requests_post(url, json=payload) + response = requests_post(url, json=payload) if not response.ok: self.log.error("Submission failed!") self.log.error(response.status_code) @@ -592,41 +634,3 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): self._instance.data["deadlineSubmissionJob"] = result return result["_id"] - - def _requests_post(self, *args, **kwargs): - """Wrap request post method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa - # add 10sec timeout before bailing out - kwargs['timeout'] = 10 - return requests.post(*args, **kwargs) - - def _requests_get(self, *args, **kwargs): - """Wrap request get method. - - Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment - variable is found. This is useful when Deadline or Muster server are - running with self-signed certificates and their certificate is not - added to trusted certificates on client machines. - - Warning: - Disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - - """ - if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa - # add 10sec timeout before bailing out - kwargs['timeout'] = 10 - return requests.get(*args, **kwargs) From 692f470d4cce099366f1e1bca13f38acbdeec4ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Jun 2021 17:46:19 +0200 Subject: [PATCH 005/359] DL expected files validation - save render job from metadata json to instance for future validation --- openpype/plugins/publish/collect_rendered_files.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_rendered_files.py b/openpype/plugins/publish/collect_rendered_files.py index edf9b50b92..2f55f2bdb5 100644 --- a/openpype/plugins/publish/collect_rendered_files.py +++ b/openpype/plugins/publish/collect_rendered_files.py @@ -87,11 +87,14 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): instance = self._context.create_instance( instance_data.get("subset") ) - self.log.info("Filling stagignDir...") + self.log.info("Filling stagingDir...") self._fill_staging_dir(instance_data, anatomy) instance.data.update(instance_data) + # stash render job id for later validation + instance.data["render_job_id"] = data.get("job").get("_id") + representations = [] for repre_data in instance_data.get("representations") or []: self._fill_staging_dir(repre_data, anatomy) From 5ed11bcb923a69901a2be94c7ec32efd5c9e9b62 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Jun 2021 17:49:11 +0200 Subject: [PATCH 006/359] DL expected files validation - added expected file validator Checks if expected files (stored in metadata json and collected) are actually rendered to limit later failure in integrate_new Allows artist override to change rendered frame range --- .../validate_expected_and_rendered_files.py | 186 ++++++++++++++++++ .../schema_project_deadline.json | 29 +++ 2 files changed, 215 insertions(+) create mode 100644 openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py new file mode 100644 index 0000000000..c71b5106ec --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -0,0 +1,186 @@ +import os +import json +import pyblish.api + +from avalon.vendor import requests + +from openpype.api import get_system_settings +from openpype.lib.abstract_submit_deadline import requests_get +from openpype.lib.delivery import collect_frames + + +class ValidateExpectedFiles(pyblish.api.InstancePlugin): + """Compare rendered and expected files""" + + label = "Validate rendered files from Deadline" + order = pyblish.api.ValidatorOrder + families = ["render"] + targets = ["deadline"] + + # check if actual frame range on render job wasn't different + # case when artists wants to render only subset of frames + allow_user_override = True + + def process(self, instance): + frame_list = self._get_frame_list(instance.data["render_job_id"]) + + for repre in instance.data["representations"]: + expected_files = self._get_expected_files(repre) + + staging_dir = repre["stagingDir"] + existing_files = self._get_existing_files(staging_dir) + + expected_non_existent = expected_files.difference( + existing_files) + if len(expected_non_existent) != 0: + self.log.info("Some expected files missing {}".format( + expected_non_existent)) + + if self.allow_user_override: + file_name_template, frame_placeholder = \ + self._get_file_name_template_and_placeholder( + expected_files) + + if not file_name_template: + return + + real_expected_rendered = self._get_real_render_expected( + file_name_template, + frame_placeholder, + frame_list) + + real_expected_non_existent = \ + real_expected_rendered.difference(existing_files) + if len(real_expected_non_existent) != 0: + raise RuntimeError("Still missing some files {}". + format(real_expected_non_existent)) + self.log.info("Update range from actual job range") + repre["files"] = sorted(list(real_expected_rendered)) + else: + raise RuntimeError("Some expected files missing {}".format( + expected_non_existent)) + + def _get_frame_list(self, original_job_id): + """ + Returns list of frame ranges from all render job. + + Render job might be requeried so job_id in metadata.json is invalid + GlobalJobPreload injects current ids to RENDER_JOB_IDS. + + Args: + original_job_id (str) + Returns: + (list) + """ + all_frame_lists = [] + render_job_ids = os.environ.get("RENDER_JOB_IDS") + if render_job_ids: + render_job_ids = render_job_ids.split(',') + else: # fallback + render_job_ids = [original_job_id] + + for job_id in render_job_ids: + job_info = self._get_job_info(job_id) + frame_list = job_info["Props"]["Frames"] + if frame_list: + all_frame_lists.extend(frame_list.split(',')) + + return all_frame_lists + + def _get_real_render_expected(self, file_name_template, frame_placeholder, + frame_list): + """ + Calculates list of names of expected rendered files. + + Might be different from job expected files if user explicitly and + manually change frame list on Deadline job. + """ + real_expected_rendered = set() + src_padding_exp = "%0{}d".format(len(frame_placeholder)) + for frames in frame_list: + if '-' not in frames: # single frame + frames = "{}-{}".format(frames, frames) + + start, end = frames.split('-') + for frame in range(int(start), int(end) + 1): + ren_name = file_name_template.replace( + frame_placeholder, src_padding_exp % frame) + real_expected_rendered.add(ren_name) + + return real_expected_rendered + + def _get_file_name_template_and_placeholder(self, files): + """Returns file name with frame replaced with # and this placeholder""" + sources_and_frames = collect_frames(files) + + file_name_template = frame_placeholder = None + for file_name, frame in sources_and_frames.items(): + frame_placeholder = "#" * len(frame) + file_name_template = os.path.basename( + file_name.replace(frame, frame_placeholder)) + break + + return file_name_template, frame_placeholder + + def _get_job_info(self, job_id): + """ + Calls DL for actual job info for 'job_id' + + Might be different than job info saved in metadata.json if user + manually changes job pre/during rendering. + """ + deadline_url = ( + get_system_settings() + ["modules"] + ["deadline"] + ["DEADLINE_REST_URL"] + ) + assert deadline_url, "Requires DEADLINE_REST_URL" + + url = "{}/api/jobs?JobID={}".format(deadline_url, job_id) + try: + response = requests_get(url) + except requests.exceptions.ConnectionError: + print("Deadline is not accessible at {}".format(deadline_url)) + # self.log("Deadline is not accessible at {}".format(deadline_url)) + return {} + + if not response.ok: + self.log.error("Submission failed!") + self.log.error(response.status_code) + self.log.error(response.content) + raise RuntimeError(response.text) + + json_content = response.json() + if json_content: + return json_content.pop() + return {} + + def _parse_metadata_json(self, json_path): + if not os.path.exists(json_path): + msg = "Metadata file {} doesn't exist".format(json_path) + raise RuntimeError(msg) + + with open(json_path) as fp: + try: + return json.load(fp) + except Exception as exc: + self.log.error( + "Error loading json: " + "{} - Exception: {}".format(json_path, exc) + ) + + def _get_existing_files(self, out_dir): + """Returns set of existing file names from 'out_dir'""" + existing_files = set() + for file_name in os.listdir(out_dir): + existing_files.add(file_name) + return existing_files + + def _get_expected_files(self, repre): + """Returns set of file names from metadata.json""" + expected_files = set() + + for file_name in repre["files"]: + expected_files.add(file_name) + return expected_files diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index d47a6917da..c88e50b40e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -11,6 +11,35 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "ValidateRenderedFrames", + "label": "Validate Rendered Frames", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "label", + "label": "Validate if all expected files were rendered" + }, + { + "type": "boolean", + "key": "allow_user_override", + "object_type": "text", + "label": "Allow user change frame range" + } + ] + }, { "type": "dict", "collapsible": true, From 3fd5d3cfdf2a5caa53ba96fa58ef668a30b4f75e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Jun 2021 17:52:21 +0200 Subject: [PATCH 007/359] DL expected files validation - added 'targets' to args to push to publish job --- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index ea953441a2..a485f432e2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -231,7 +231,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): args = [ 'publish', - roothless_metadata_path + roothless_metadata_path, + "--targets {}".format("deadline") ] # Generate the payload for Deadline submission From 8bb49a5ca82385623fd18d06acf045e96c8e98ea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Jun 2021 11:56:38 +0200 Subject: [PATCH 008/359] DL expected files validation - fixed schema and added defaults --- .../defaults/project_settings/deadline.json | 7 +++++++ .../projects_schema/schema_project_deadline.json | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 03f3e19a64..2cc345d5ad 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -1,5 +1,12 @@ { "publish": { + "ValidateExpectedFiles": { + "enabled": true, + "active": true, + "families": ["render"], + "targets": ["deadline"], + "allow_user_override": true + }, "MayaSubmitDeadline": { "enabled": true, "optional": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index c88e50b40e..f6a8127951 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -14,8 +14,8 @@ { "type": "dict", "collapsible": true, - "key": "ValidateRenderedFrames", - "label": "Validate Rendered Frames", + "key": "ValidateExpectedFiles", + "label": "Validate Expected Files", "checkbox_key": "enabled", "children": [ { @@ -37,6 +37,18 @@ "key": "allow_user_override", "object_type": "text", "label": "Allow user change frame range" + }, + { + "type": "list", + "key": "families", + "object_type": "text", + "label": "Trigger on families" + }, + { + "type": "list", + "key": "targets", + "object_type": "text", + "label": "Trigger for plugins" } ] }, From 08bf7e5b7c6e3a244ed1dbfb5aa7e663b5db8933 Mon Sep 17 00:00:00 2001 From: jezscha Date: Tue, 8 Jun 2021 09:59:30 +0000 Subject: [PATCH 009/359] Create draft PR for #1658 From c4fb8fdbe1ea45ce0fcbed33faf25860e4a982c1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Jun 2021 12:36:50 +0200 Subject: [PATCH 010/359] Global: fix duplicity of plugin --- .../integrate_ftrack_component_overwrite.py | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 openpype/plugins/publish/integrate_ftrack_component_overwrite.py diff --git a/openpype/plugins/publish/integrate_ftrack_component_overwrite.py b/openpype/plugins/publish/integrate_ftrack_component_overwrite.py deleted file mode 100644 index 047fd8462c..0000000000 --- a/openpype/plugins/publish/integrate_ftrack_component_overwrite.py +++ /dev/null @@ -1,21 +0,0 @@ -import pyblish.api - - -class IntegrateFtrackComponentOverwrite(pyblish.api.InstancePlugin): - """ - Set `component_overwrite` to True on all instances `ftrackComponentsList` - """ - - order = pyblish.api.IntegratorOrder + 0.49 - label = 'Overwrite ftrack created versions' - families = ["clip"] - optional = True - active = False - - def process(self, instance): - component_list = instance.data['ftrackComponentsList'] - - for cl in component_list: - cl['component_overwrite'] = True - self.log.debug('Component {} overwriting'.format( - cl['component_data']['name'])) From 0fea8b201c52bc02e5ee4b41d2c6c23e2c06b67b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Jun 2021 18:06:25 +0200 Subject: [PATCH 011/359] Global: validator plugin editorial asset names --- .../publish/validate_editorial_asset_name.py | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 openpype/plugins/publish/validate_editorial_asset_name.py diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py new file mode 100644 index 0000000000..6548b8ea89 --- /dev/null +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -0,0 +1,113 @@ +import pyblish.api +from avalon import io +from pprint import pformat + + +class ValidateEditorialAssetName(pyblish.api.ContextPlugin): + """ Validating if editorial's asset names are not already created in db. + + Checking variations of names with different size of caps or with or without underscores. + """ + + order = pyblish.api.ValidatorOrder + label = "Validate Asset Name" + + def process(self, context): + + asset_and_parents = self.get_parents(context) + + if not io.Session: + io.install() + + db_assets = list(io.find( + {"type": "asset"}, {"name": 1, "data.parents": 1})) + self.log.debug("__ db_assets: {}".format(db_assets)) + + project_entities = { + str(e["name"]): e["data"]["parents"] for e in db_assets} + + self.log.debug("__ project_entities: {}".format( + pformat(project_entities))) + + assets_missing_name = {} + assets_wrong_parent = {} + for asset in asset_and_parents.keys(): + if asset not in project_entities.keys(): + # add to some nonexistent list for next layer of check + assets_missing_name.update({asset: asset_and_parents[asset]}) + continue + + if asset_and_parents[asset] != project_entities[asset]: + # add to some nonexistent list for next layer of check + assets_wrong_parent.update({ + asset: { + "required": asset_and_parents[asset], + "already_in_db": project_entities[asset] + } + }) + continue + + self.log.info("correct asset: {}".format(asset)) + + if assets_missing_name: + wrong_names = {} + self.log.debug( + ">> assets_missing_name: {}".format(assets_missing_name)) + for asset in assets_missing_name.keys(): + _asset = asset.lower().replace("_", "") + if _asset in [a.lower().replace("_", "") + for a in project_entities.keys()]: + wrong_names.update({ + "required_name": asset, + "used_variants_in_db": [ + a for a in project_entities.keys() + if a.lower().replace("_", "") == _asset + ] + }) + + if wrong_names: + self.log.debug( + ">> wrong_names: {}".format(wrong_names)) + raise Exception( + "Some already existing asset name variants `{}`".format( + wrong_names)) + + + + if assets_wrong_parent: + self.log.debug( + ">> assets_wrong_parent: {}".format(assets_wrong_parent)) + raise Exception( + "Wrong parents on assets `{}`".format(assets_wrong_parent)) + + def _get_all_assets(self, input_dict): + """ Returns asset names in list. + + List contains all asset names including parents + """ + for key in input_dict.keys(): + # check if child key is available + if input_dict[key].get("childs"): + # loop deeper + self._get_all_assets( + input_dict[key]["childs"]) + else: + self.all_testing_assets.append(key) + + def get_parents(self, context): + return_dict = {} + for instance in context: + asset = instance.data["asset"] + families = instance.data.get("families", []) + [ + instance.data["family"] + ] + # filter out non-shot families + if "shot" not in families: + continue + + parents = instance.data["parents"] + + return_dict.update({ + asset: [p["entity_name"] for p in parents] + }) + return return_dict From 7e164c8a470b4a1a7a91f153b00549443297b50f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 8 Jun 2021 18:11:35 +0200 Subject: [PATCH 012/359] hound: suggestions --- openpype/plugins/publish/validate_editorial_asset_name.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index 6548b8ea89..60b8f76a07 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -6,7 +6,8 @@ from pprint import pformat class ValidateEditorialAssetName(pyblish.api.ContextPlugin): """ Validating if editorial's asset names are not already created in db. - Checking variations of names with different size of caps or with or without underscores. + Checking variations of names with different size of caps or with + or without underscores. """ order = pyblish.api.ValidatorOrder @@ -72,8 +73,6 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): "Some already existing asset name variants `{}`".format( wrong_names)) - - if assets_wrong_parent: self.log.debug( ">> assets_wrong_parent: {}".format(assets_wrong_parent)) From 8b0036cf53426068ac855f411e18b3e6ab418c30 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 9 Jun 2021 18:19:40 +0200 Subject: [PATCH 013/359] fix unreal launch on linux --- openpype/hosts/unreal/api/lib.py | 212 +++++++++++------- .../unreal/hooks/pre_workfile_preparation.py | 10 +- 2 files changed, 139 insertions(+), 83 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 7e706a2789..3efc0c63be 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -1,38 +1,50 @@ +# -*- coding: utf-8 -*- +"""Unreal launching and project tools.""" import sys import os import platform import json from distutils import dir_util import subprocess +import re +from collections import OrderedDict from openpype.api import get_project_settings -def get_engine_versions(): - """ +def get_engine_versions(env=None): + """Detect Unreal Engine versions. + This will try to detect location and versions of installed Unreal Engine. Location can be overridden by `UNREAL_ENGINE_LOCATION` environment variable. - Returns: + Args: + env (dict, optional): Environment to use. - dict: dictionary with version as a key and dir as value. + Returns: + OrderedDict: dictionary with version as a key and dir as value. + so the highest version is first. Example: - - >>> get_engine_version() + >>> get_engine_versions() { "4.23": "C:/Epic Games/UE_4.23", "4.24": "C:/Epic Games/UE_4.24" } - """ - try: - engine_locations = {} - root, dirs, files = next(os.walk(os.environ["UNREAL_ENGINE_LOCATION"])) - for dir in dirs: - if dir.startswith("UE_"): - ver = dir.split("_")[1] - engine_locations[ver] = os.path.join(root, dir) + """ + env = env or os.environ + engine_locations = {} + try: + root, dirs, _ = next(os.walk(env["UNREAL_ENGINE_LOCATION"])) + + for directory in dirs: + if directory.startswith("UE"): + try: + ver = re.split(r"[-_]", directory)[1] + except IndexError: + continue + engine_locations[ver] = os.path.join(root, directory) except KeyError: # environment variable not set pass @@ -40,32 +52,37 @@ def get_engine_versions(): # specified directory doesn't exists pass - # if we've got something, terminate autodetection process + # if we've got something, terminate auto-detection process if engine_locations: - return engine_locations + return OrderedDict(sorted(engine_locations.items())) # else kick in platform specific detection if platform.system().lower() == "windows": - return _win_get_engine_versions() + return OrderedDict(sorted(_win_get_engine_versions().items())) elif platform.system().lower() == "linux": # on linux, there is no installation and getting Unreal Engine involves # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. pass elif platform.system().lower() == "darwin": - return _darwin_get_engine_version() + return OrderedDict(sorted(_darwin_get_engine_version(env).items())) - return {} + return OrderedDict() def _win_get_engine_versions(): - """ + """Get Unreal Engine versions on Windows. + If engines are installed via Epic Games Launcher then there is: `%PROGRAMDATA%/Epic/UnrealEngineLauncher/LauncherInstalled.dat` This file is JSON file listing installed stuff, Unreal engines are marked with `"AppName" = "UE_X.XX"`` like `UE_4.24` + + Returns: + dict: version as a key and path as a value. + """ install_json_path = os.path.join( - os.environ.get("PROGRAMDATA"), + os.getenv("PROGRAMDATA"), "Epic", "UnrealEngineLauncher", "LauncherInstalled.dat", @@ -75,11 +92,19 @@ def _win_get_engine_versions(): def _darwin_get_engine_version() -> dict: - """ + """Get Unreal Engine versions on MacOS. + It works the same as on Windows, just JSON file location is different. + + Returns: + dict: version as a key and path as a value. + + See Aslo: + :func:`_win_get_engine_versions`. + """ install_json_path = os.path.join( - os.environ.get("HOME"), + os.getenv("HOME"), "Library", "Application Support", "Epic", @@ -91,25 +116,26 @@ def _darwin_get_engine_version() -> dict: def _parse_launcher_locations(install_json_path: str) -> dict: - """ - This will parse locations from json file. + """This will parse locations from json file. + + Args: + install_json_path (str): Path to `LauncherInstalled.dat`. + + Returns: + dict: with unreal engine versions as keys and + paths to those engine installations as value. - :param install_json_path: path to `LauncherInstalled.dat` - :type install_json_path: str - :returns: returns dict with unreal engine versions as keys and - paths to those engine installations as value. - :rtype: dict """ engine_locations = {} if os.path.isfile(install_json_path): with open(install_json_path, "r") as ilf: try: install_data = json.load(ilf) - except json.JSONDecodeError: + except json.JSONDecodeError as e: raise Exception( "Invalid `LauncherInstalled.dat file. `" "Cannot determine Unreal Engine location." - ) + ) from e for installation in install_data.get("InstallationList", []): if installation.get("AppName").startswith("UE_"): @@ -123,36 +149,43 @@ def create_unreal_project(project_name: str, ue_version: str, pr_dir: str, engine_path: str, - dev_mode: bool = False) -> None: - """ - This will create `.uproject` file at specified location. As there is no - way I know to create project via command line, this is easiest option. - Unreal project file is basically JSON file. If we find + dev_mode: bool = False, + env: dict = None) -> None: + """This will create `.uproject` file at specified location. + + As there is no way I know to create project via command line, this is + easiest option. Unreal project file is basically JSON file. If we find `AVALON_UNREAL_PLUGIN` environment variable we assume this is location of Avalon Integration Plugin and we copy its content to project folder and enable this plugin. - :param project_name: project name - :type project_name: str - :param ue_version: unreal engine version (like 4.23) - :type ue_version: str - :param pr_dir: path to directory where project will be created - :type pr_dir: str - :param engine_path: Path to Unreal Engine installation - :type engine_path: str - :param dev_mode: Flag to trigger C++ style Unreal project needing - Visual Studio and other tools to compile plugins from - sources. This will trigger automatically if `Binaries` - directory is not found in plugin folders as this indicates - this is only source distribution of the plugin. Dev mode - is also set by preset file `unreal/project_setup.json` in - **OPENPYPE_CONFIG**. - :type dev_mode: bool - :returns: None - """ - preset = get_project_settings(project_name)["unreal"]["project_setup"] + Args: + project_name (str): Name of the project. + ue_version (str): Unreal engine version (like 4.23). + pr_dir (str): Path to directory where project will be created. + engine_path (str): Path to Unreal Engine installation. + dev_mode (bool, optional): Flag to trigger C++ style Unreal project + needing Visual Studio and other tools to compile plugins from + sources. This will trigger automatically if `Binaries` + directory is not found in plugin folders as this indicates + this is only source distribution of the plugin. Dev mode + is also set by preset file `unreal/project_setup.json` in + **OPENPYPE_CONFIG**. + env (dict, optional): Environment to use. If not set, `os.environ`. - if os.path.isdir(os.environ.get("AVALON_UNREAL_PLUGIN", "")): + Throws: + NotImplemented: For unsupported platforms. + + Returns: + None + + """ + env = env or os.environ + preset = get_project_settings(project_name)["unreal"]["project_setup"] + plugins_path = None + uep_path = None + + if os.path.isdir(env.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project plugins_path = os.path.join(pr_dir, "Plugins") avalon_plugin_path = os.path.join(plugins_path, "Avalon") @@ -180,17 +213,17 @@ def create_unreal_project(project_name: str, } if preset["install_unreal_python_engine"]: - # If `OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there - # to support offline installation. + # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to + # support offline installation. # Otherwise clone UnrealEnginePython to Plugins directory # https://github.com/20tab/UnrealEnginePython.git uep_path = os.path.join(plugins_path, "UnrealEnginePython") - if os.environ.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + if env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): os.makedirs(uep_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree( - os.environ.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), uep_path) else: # WARNING: this will trigger dev_mode, because we need to compile @@ -246,25 +279,40 @@ def create_unreal_project(project_name: str, with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) - # UE < 4.26 have Python2 by default, so we need PySide - # but we will not need it in 4.26 and up - if int(ue_version.split(".")[1]) < 26: - # ensure we have PySide installed in engine - # TODO: make it work for other platforms 🍎 🐧 + # ensure we have PySide installed in engine + # this won't work probably as pyside is no longer on pypi + # DEPRECATED: support for python 2 in UE4 is dropped. + python_path = None + if int(ue_version.split(".")[0]) == 4 and \ + int(ue_version.split(".")[1]) < 25: if platform.system().lower() == "windows": python_path = os.path.join(engine_path, "Engine", "Binaries", "ThirdParty", "Python", "Win64", "python.exe") + if platform.system().lower() == "linux": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python", "Linux", + "bin", "python") + + if platform.system().lower() == "darwin": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python", "Mac", + "bin", "python") + + if python_path: subprocess.run([python_path, "-m", "pip", "install", "pyside"]) + else: + raise NotImplemented("Unsupported platform") if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) def _prepare_cpp_project(project_file: str, engine_path: str) -> None: - """ + """Prepare CPP Unreal Project. + This function will add source files needed for project to be rebuild along with the avalon integration plugin. @@ -273,12 +321,12 @@ def _prepare_cpp_project(project_file: str, engine_path: str) -> None: by some generator. This needs more research as manually writing those files is rather hackish. :skull_and_crossbones: - :param project_file: path to .uproject file - :type project_file: str - :param engine_path: path to unreal engine associated with project - :type engine_path: str - """ + Args: + project_file (str): Path to .uproject file. + engine_path (str): Path to unreal engine associated with project. + + """ project_name = os.path.splitext(os.path.basename(project_file))[0] project_dir = os.path.dirname(project_file) targets_dir = os.path.join(project_dir, "Source") @@ -388,19 +436,23 @@ class {1}_API A{0}GameModeBase : public AGameModeBase sources_dir, f"{project_name}GameModeBase.h"), mode="w") as f: f.write(game_mode_h) + u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" + "UnrealBuildTool.exe") + u_header_tool = None + if platform.system().lower() == "windows": - u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" - "UnrealBuildTool.exe") u_header_tool = (f"{engine_path}/Engine/Binaries/Win64/" f"UnrealHeaderTool.exe") elif platform.system().lower() == "linux": - # WARNING: there is no UnrealBuildTool on linux? - u_build_tool = "" - u_header_tool = "" + u_header_tool = (f"{engine_path}/Engine/Binaries/Linux/" + f"UnrealHeaderTool") elif platform.system().lower() == "darwin": - # WARNING: there is no UnrealBuildTool on Mac? - u_build_tool = "" - u_header_tool = "" + # we need to test this out + u_header_tool = (f"{engine_path}/Engine/Binaries/Mac/" + f"UnrealHeaderTool") + + if not u_header_tool: + raise NotImplemented("Unsupported platform") u_build_tool = u_build_tool.replace("\\", "/") u_header_tool = u_header_tool.replace("\\", "/") diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index f084cccfc3..7c4b6c3088 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Hook to launch Unreal and prepare projects.""" import os from openpype.lib import ( @@ -8,19 +10,21 @@ from openpype.hosts.unreal.api import lib as unreal_lib class UnrealPrelaunchHook(PreLaunchHook): - """ + """Hook to handle launching Unreal. + 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. - """ + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.signature = "( {} )".format(self.__class__.__name__) def execute(self): + """Hook entry method.""" asset_name = self.data["asset_name"] task_name = self.data["task_name"] workdir = self.launch_context.env["AVALON_WORKDIR"] @@ -52,7 +56,7 @@ class UnrealPrelaunchHook(PreLaunchHook): f"[ {engine_version} ]" )) - detected = unreal_lib.get_engine_versions() + detected = unreal_lib.get_engine_versions(self.launch_context.env) detected_str = ', '.join(detected.keys()) or 'none' self.log.info(( f"{self.signature} detected UE4 versions: " From 969c32abd60fe66bebe04a5664aae1b79fd80187 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 9 Jun 2021 18:24:38 +0200 Subject: [PATCH 014/359] fix exceptions --- openpype/hosts/unreal/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 3efc0c63be..269ba561c7 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -174,7 +174,7 @@ def create_unreal_project(project_name: str, env (dict, optional): Environment to use. If not set, `os.environ`. Throws: - NotImplemented: For unsupported platforms. + NotImplementedError: For unsupported platforms. Returns: None @@ -304,7 +304,7 @@ def create_unreal_project(project_name: str, subprocess.run([python_path, "-m", "pip", "install", "pyside"]) else: - raise NotImplemented("Unsupported platform") + raise NotImplementedError("Unsupported platform") if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) @@ -452,7 +452,7 @@ class {1}_API A{0}GameModeBase : public AGameModeBase f"UnrealHeaderTool") if not u_header_tool: - raise NotImplemented("Unsupported platform") + raise NotImplementedError("Unsupported platform") u_build_tool = u_build_tool.replace("\\", "/") u_header_tool = u_header_tool.replace("\\", "/") From 00336a9d2d6fc0bd9a16a0b9dfb44a0bfe4af641 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 9 Jun 2021 18:27:02 +0200 Subject: [PATCH 015/359] refactor ifs --- openpype/hosts/unreal/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 269ba561c7..e069ac5256 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -59,12 +59,12 @@ def get_engine_versions(env=None): # else kick in platform specific detection if platform.system().lower() == "windows": return OrderedDict(sorted(_win_get_engine_versions().items())) - elif platform.system().lower() == "linux": + if platform.system().lower() == "linux": # on linux, there is no installation and getting Unreal Engine involves # git clone. So we'll probably depend on `UNREAL_ENGINE_LOCATION`. pass - elif platform.system().lower() == "darwin": - return OrderedDict(sorted(_darwin_get_engine_version(env).items())) + if platform.system().lower() == "darwin": + return OrderedDict(sorted(_darwin_get_engine_version().items())) return OrderedDict() From 8fc0452438ff76b57d59d4d0c36a6934894e0e51 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:04:04 +0200 Subject: [PATCH 016/359] implemented UndefinedApplicationExecutable which is used if application does not have "executables" key --- openpype/lib/applications.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index d82b7cd847..074c722a81 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -484,6 +484,27 @@ class ApplicationExecutable: return bool(self._realpath()) +class UndefinedApplicationExecutable(ApplicationExecutable): + """Some applications do not require executable path from settings. + + In that case this class is used to "fake" existing executable. + """ + def __init__(self): + pass + + def __str__(self): + return self.__class__.__name__ + + def __repr__(self): + return "<{}>".format(self.__class__.__name__) + + def as_args(self): + return [] + + def exists(self): + return True + + @six.add_metaclass(ABCMeta) class LaunchHook: """Abstract base class of launch hook.""" From f694275bc4a9cd7c1605fdacaa0f6aedcac38ccb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:04:37 +0200 Subject: [PATCH 017/359] local settings will skip applications without "executables" key --- openpype/settings/lib.py | 6 +++++- openpype/tools/settings/local_settings/apps_widget.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index f61166fa69..4a3e66de33 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -532,7 +532,11 @@ def apply_local_settings_on_system_settings(system_settings, local_settings): variants = system_settings["applications"][app_group_name]["variants"] for app_name, app_value in value.items(): - if not app_value or app_name not in variants: + if ( + not app_value + or app_name not in variants + or "executables" not in variants[app_name] + ): continue executable = app_value.get("executable") diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index 5f4e5dd1c5..f228ecc826 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -121,6 +121,9 @@ class AppGroupWidget(QtWidgets.QWidget): widgets_by_variant_name = {} for variant_name, variant_entity in valid_variants.items(): + if "executables" not in variant_entity: + continue + variant_widget = AppVariantWidget( group_label, variant_name, variant_entity, content_widget ) From 5b9e8bb495eafc759a9e403b1056747caf2155cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:08:06 +0200 Subject: [PATCH 018/359] Application can use `UndefinedApplicationExecutable` --- openpype/lib/applications.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 074c722a81..bed57d7022 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -191,26 +191,32 @@ class Application: self.full_label = full_label self._environment = data.get("environment") or {} + arguments = data.get("arguments") + if isinstance(arguments, dict): + arguments = arguments.get(platform.system().lower()) + + if not arguments: + arguments = [] + self.arguments = arguments + + if "executables" not in data: + self.executables = [ + UndefinedApplicationExecutable() + ] + return + _executables = data["executables"] + if isinstance(_executables, dict): + _executables = _executables.get(platform.system().lower()) + if not _executables: _executables = [] - elif isinstance(_executables, dict): - _executables = _executables.get(platform.system().lower()) or [] - - _arguments = data["arguments"] - if not _arguments: - _arguments = [] - - elif isinstance(_arguments, dict): - _arguments = _arguments.get(platform.system().lower()) or [] - executables = [] for executable in _executables: executables.append(ApplicationExecutable(executable)) self.executables = executables - self.arguments = _arguments def __repr__(self): return "<{}> - {}".format(self.__class__.__name__, self.full_name) From 5432a264653866cb00a3309b2b8a8679aea836da Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:08:38 +0200 Subject: [PATCH 019/359] skip executables and arguments key for unreal application --- .../defaults/system_settings/applications.json | 10 ---------- .../system_schema/host_settings/schema_unreal.json | 6 +++++- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 020924db67..72cd010cf2 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1101,16 +1101,6 @@ "variants": { "4-26": { "use_python_2": false, - "executables": { - "windows": [], - "darwin": [], - "linux": [] - }, - "arguments": { - "windows": [], - "darwin": [], - "linux": [] - }, "environment": {} } } diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json index df5ec0e6fa..fa1b45601f 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json @@ -30,7 +30,11 @@ "children": [ { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": [ + "executables", + "arguments" + ] } ] } From b342760446cb1dfc7b62ec5cba6b58370a0ba7ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 10 Jun 2021 13:08:47 +0200 Subject: [PATCH 020/359] named separator so can be skipped --- .../schemas/system_schema/host_settings/schema_unreal.json | 1 + .../system_schema/host_settings/template_host_variant_items.json | 1 + 2 files changed, 2 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json index fa1b45601f..133d6c9eaf 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_unreal.json @@ -33,6 +33,7 @@ "name": "template_host_variant_items", "skip_paths": [ "executables", + "separator", "arguments" ] } diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json index ab4d2374a3..409efb006e 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant_items.json @@ -14,6 +14,7 @@ "placeholder": "Executable path" }, { + "key": "separator", "type":"separator" }, { From 5de11adbcb8c56ab16c32cb01b57f5b19b77e346 Mon Sep 17 00:00:00 2001 From: jezscha Date: Fri, 11 Jun 2021 08:15:41 +0000 Subject: [PATCH 021/359] Create draft PR for #1587 From 35192876dc3a4d643a45c7742456ce28de1bf538 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Jun 2021 10:26:18 +0200 Subject: [PATCH 022/359] Global: suggested namespace change --- .../publish/validate_editorial_asset_name.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/validate_editorial_asset_name.py b/openpype/plugins/publish/validate_editorial_asset_name.py index 60b8f76a07..ccea42dc37 100644 --- a/openpype/plugins/publish/validate_editorial_asset_name.py +++ b/openpype/plugins/publish/validate_editorial_asset_name.py @@ -24,26 +24,26 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): {"type": "asset"}, {"name": 1, "data.parents": 1})) self.log.debug("__ db_assets: {}".format(db_assets)) - project_entities = { + asset_db_docs = { str(e["name"]): e["data"]["parents"] for e in db_assets} self.log.debug("__ project_entities: {}".format( - pformat(project_entities))) + pformat(asset_db_docs))) assets_missing_name = {} assets_wrong_parent = {} for asset in asset_and_parents.keys(): - if asset not in project_entities.keys(): + if asset not in asset_db_docs.keys(): # add to some nonexistent list for next layer of check assets_missing_name.update({asset: asset_and_parents[asset]}) continue - if asset_and_parents[asset] != project_entities[asset]: + if asset_and_parents[asset] != asset_db_docs[asset]: # add to some nonexistent list for next layer of check assets_wrong_parent.update({ asset: { "required": asset_and_parents[asset], - "already_in_db": project_entities[asset] + "already_in_db": asset_db_docs[asset] } }) continue @@ -57,11 +57,11 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): for asset in assets_missing_name.keys(): _asset = asset.lower().replace("_", "") if _asset in [a.lower().replace("_", "") - for a in project_entities.keys()]: + for a in asset_db_docs.keys()]: wrong_names.update({ "required_name": asset, "used_variants_in_db": [ - a for a in project_entities.keys() + a for a in asset_db_docs.keys() if a.lower().replace("_", "") == _asset ] }) From 5be6803ace63b4bec26f8c898bcf2fb3080e95c6 Mon Sep 17 00:00:00 2001 From: antirotor Date: Fri, 11 Jun 2021 09:53:33 +0000 Subject: [PATCH 023/359] Create draft PR for #1665 From 3addbe5ba6b746dfbbce18cd7c810d7cd6aeaab4 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 11 Jun 2021 12:30:41 +0200 Subject: [PATCH 024/359] handle invalid version in zip gracefully --- igniter/bootstrap_repos.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 7c4f8b4b69..6eaea27116 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -972,8 +972,12 @@ class BootstrapRepos: "openpype/version.py") as version_file: zip_version = {} exec(version_file.read(), zip_version) - version_check = OpenPypeVersion( - version=zip_version["__version__"]) + try: + version_check = OpenPypeVersion( + version=zip_version["__version__"]) + except ValueError as e: + self._print(str(e), True) + return False version_main = version_check.get_main_version() # noqa: E501 detected_main = detected_version.get_main_version() # noqa: E501 From 76a91ba186c9b7bd1f0441c94fd55937bbb71ad1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Jun 2021 18:24:20 +0200 Subject: [PATCH 025/359] Global: adding otio video trimming --- .../publish/collect_otio_subset_resources.py | 20 ++- .../publish/extract_otio_trimming_video.py | 128 ++++++++++++++++++ 2 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 openpype/plugins/publish/extract_otio_trimming_video.py diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index fa376eeac7..010430a303 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -40,6 +40,7 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): otio_clip = instance.data["otioClip"] otio_avalable_range = otio_clip.available_range() media_fps = otio_avalable_range.start_time.rate + available_duration = otio_avalable_range.duration.value # get available range trimmed with processed retimes retimed_attributes = editorial.get_media_range_with_retimes( @@ -68,6 +69,8 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): a_frame_start_h, (a_frame_end_h - a_frame_start_h + 1), media_fps ) + trimmed_duration = trimmed_media_range_h.duration.value + self.log.debug("trimmed_media_range_h: {}".format( trimmed_media_range_h)) self.log.debug("a_frame_start_h: {}".format( @@ -150,12 +153,18 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): repre = self._create_representation( frame_start, frame_end, collection=collection) else: + _trim = False dirname, filename = os.path.split(media_ref.target_url) self.staging_dir = dirname + if trimmed_duration < available_duration: + self.log.debug("Ready for Trimming") + instance.data["families"].append("trim") + instance.data["otioTrimmingRange"] = trimmed_media_range_h + _trim = True self.log.debug(filename) repre = self._create_representation( - frame_start, frame_end, file=filename) + frame_start, frame_end, file=filename, trim=_trim) if repre: # add representation to instance data @@ -196,7 +205,7 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): "frameStart": start, "frameEnd": end, }) - return representation_data + if kwargs.get("file"): file = kwargs.get("file") ext = os.path.splitext(file)[-1] @@ -207,4 +216,9 @@ class CollectOcioSubsetResources(pyblish.api.InstancePlugin): "frameStart": start, "frameEnd": end, }) - return representation_data + + if kwargs.get("trim") is True: + representation_data.update({ + "tags": ["trim"] + }) + return representation_data diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py new file mode 100644 index 0000000000..f197c958ee --- /dev/null +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -0,0 +1,128 @@ +""" +Requires: + instance -> otioTrimmingRange + instance -> representations + +""" + +import os +from pyblish import api +import openpype +from copy import deepcopy + + +class ExtractOTIOTrimmingVideo(openpype.api.Extractor): + """ + Trimming video file longer then required lenght + + """ + order = api.ExtractorOrder + label = "Extract OTIO trim longer video" + families = ["trim"] + hosts = ["resolve", "hiero"] + + def process(self, instance): + self.staging_dir = self.staging_dir(instance) + otio_trim_range = instance.data["otioTrimmingRange"] + representations = instance.data["representations"] + self.log.debug("otio_trim_range: {}".format(otio_trim_range)) + self.log.debug("self.staging_dir: {}".format(self.staging_dir)) + + # get corresponding representation + for _repre in representations: + if "trim" not in _repre.get("tags"): + continue + + input_file = _repre["files"] + input_file_path = os.path.normpath(os.path.join( + _repre["stagingDir"], input_file + )) + self.log.debug("input_file_path: {}".format(input_file_path)) + + # trim via ffmpeg + new_file = self._ffmpeg_trim_seqment( + input_file_path, otio_trim_range) + + # prepare new representation data + repre_data = deepcopy(_repre) + # remove tags as we dont need them + repre_data.pop("tags") + repre_data["stagingDir"] = self.staging_dir + repre_data["files"] = new_file + + # romove `trim` tagged representation + representations.remove(_repre) + representations.append(repre_data) + self.log.debug(repre_data) + + self.log.debug("representations: {}".format(representations)) + + + + def _ffmpeg_trim_seqment(self, input_file_path, otio_range): + """ + Trim seqment of video file. + + Using ffmpeg to trim video to desired length. + + Args: + input_file_path (str): path string + otio_range (opentime.TimeRange): range to trim to + + """ + # get rendering app path + ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") + + # create path to destination + output_path = self._get_ffmpeg_output(input_file_path) + + # start command list + command = ['"{}"'.format(ffmpeg_path)] + + video_path = input_file_path + frame_start = otio_range.start_time.value + input_fps = otio_range.start_time.rate + frame_duration = (otio_range.duration.value + 1) + sec_start = openpype.lib.frames_to_secons(frame_start, input_fps) + sec_duration = openpype.lib.frames_to_secons(frame_duration, input_fps) + + # form command for rendering gap files + command.extend([ + "-ss {}".format(sec_start), + "-t {}".format(sec_duration), + "-i \"{}\"".format(video_path), + "-c copy", + output_path + ]) + + # execute + self.log.debug("Executing: {}".format(" ".join(command))) + output = openpype.api.run_subprocess( + " ".join(command), logger=self.log + ) + self.log.debug("Output: {}".format(output)) + + return os.path.basename(output_path) + + + def _get_ffmpeg_output(self, file_path): + """ + Returning ffmpeg output command arguments. + + Arguments" + file_path (str): path string + + Returns: + str: output_path is path + + """ + basename = os.path.basename(file_path) + name, ext = os.path.splitext(basename) + + output_file = "{}_{}{}".format( + name, + "trimmed", + ext + ) + # create path to destination + return os.path.join(self.staging_dir, output_file) From 47f27cc387f4636468c51445e1ca982ebf9d8ed3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Jun 2021 18:24:49 +0200 Subject: [PATCH 026/359] Hiero: fixing audio return bug --- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 92e0a70d15..6e5c640e35 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -41,10 +41,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # process all sellected timeline track items for track_item in selected_timeline_items: - data = {} clip_name = track_item.name() source_clip = track_item.source() + self.log.debug("clip_name: {}".format(clip_name)) # get clips subtracks and anotations annotations = self.clip_annotations(source_clip) @@ -128,7 +128,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "_ instance.data: {}".format(pformat(instance.data))) if not with_audio: - return + continue # create audio subset instance self.create_audio_instance(context, **data) From e417ffd9538d5f14f0396c7aac5b522cb032d6e4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 11 Jun 2021 18:27:26 +0200 Subject: [PATCH 027/359] hound: suggestions --- openpype/plugins/publish/extract_otio_trimming_video.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index f197c958ee..5bcda1dfcc 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -57,8 +57,6 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): self.log.debug("representations: {}".format(representations)) - - def _ffmpeg_trim_seqment(self, input_file_path, otio_range): """ Trim seqment of video file. @@ -104,7 +102,6 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): return os.path.basename(output_path) - def _get_ffmpeg_output(self, file_path): """ Returning ffmpeg output command arguments. From 41313894ea8468e16c481b8edae268703ed89eaf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:41:32 +0200 Subject: [PATCH 028/359] timers manager use openpype style for message box --- openpype/modules/timers_manager/widget_user_idle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/timers_manager/widget_user_idle.py b/openpype/modules/timers_manager/widget_user_idle.py index 8b614f6a13..25b4e56650 100644 --- a/openpype/modules/timers_manager/widget_user_idle.py +++ b/openpype/modules/timers_manager/widget_user_idle.py @@ -1,6 +1,5 @@ -from avalon import style from Qt import QtCore, QtGui, QtWidgets -from openpype import resources +from openpype import resources, style class WidgetUserIdle(QtWidgets.QWidget): From ef776d9c0ec639b3369f33a247ad55ecc9c16082 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:41:55 +0200 Subject: [PATCH 029/359] muster login widget is using openpype style --- openpype/modules/muster/widget_login.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/modules/muster/widget_login.py b/openpype/modules/muster/widget_login.py index d9af4cb99f..7c77857935 100644 --- a/openpype/modules/muster/widget_login.py +++ b/openpype/modules/muster/widget_login.py @@ -1,13 +1,12 @@ import os from Qt import QtCore, QtGui, QtWidgets -from avalon import style -from openpype import resources +from openpype import resources, style class MusterLogin(QtWidgets.QWidget): SIZE_W = 300 - SIZE_H = 130 + SIZE_H = 150 loginSignal = QtCore.Signal(object, object, object) From 923e8e8df95ee72b863659be608d4f29ed952a09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:42:22 +0200 Subject: [PATCH 030/359] buttons in muster login have with same width --- openpype/modules/muster/widget_login.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/muster/widget_login.py b/openpype/modules/muster/widget_login.py index 7c77857935..231b52c6bd 100644 --- a/openpype/modules/muster/widget_login.py +++ b/openpype/modules/muster/widget_login.py @@ -122,7 +122,6 @@ class MusterLogin(QtWidgets.QWidget): super().keyPressEvent(key_event) def setError(self, msg): - self.error_label.setText(msg) self.error_label.show() @@ -148,6 +147,17 @@ class MusterLogin(QtWidgets.QWidget): def save_credentials(self, username, password): self.module.get_auth_token(username, password) + def showEvent(self, event): + super(MusterLogin, self).showEvent(event) + + # Make btns same width + max_width = max( + self.btn_ok.sizeHint().width(), + self.btn_cancel.sizeHint().width() + ) + self.btn_ok.setMinimumWidth(max_width) + self.btn_cancel.setMinimumWidth(max_width) + def closeEvent(self, event): event.ignore() self._close_widget() From 8049fcdace72c77f49a057d6c58100115f0f3962 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:42:38 +0200 Subject: [PATCH 031/359] ftrack login use openpype style --- openpype/modules/ftrack/tray/login_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index a6360a7380..6a5d0ea786 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -1,6 +1,6 @@ import os import requests -from avalon import style +from openpype import style from openpype.modules.ftrack.lib import credentials from . import login_tools from openpype import resources From e711af6118a08484e24800a290f1b06ab818cf73 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:42:54 +0200 Subject: [PATCH 032/359] ftrack url is label instead of input (as user can't change it) --- openpype/modules/ftrack/tray/login_dialog.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py index 6a5d0ea786..cc5689bee5 100644 --- a/openpype/modules/ftrack/tray/login_dialog.py +++ b/openpype/modules/ftrack/tray/login_dialog.py @@ -46,8 +46,11 @@ class CredentialsDialog(QtWidgets.QDialog): self.user_label = QtWidgets.QLabel("Username:") self.api_label = QtWidgets.QLabel("API Key:") - self.ftsite_input = QtWidgets.QLineEdit() - self.ftsite_input.setReadOnly(True) + self.ftsite_input = QtWidgets.QLabel() + self.ftsite_input.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + # self.ftsite_input.setReadOnly(True) self.ftsite_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) self.user_input = QtWidgets.QLineEdit() From e23d56d7aa7fd70c71aa7071aa30704014ffa8b9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:43:27 +0200 Subject: [PATCH 033/359] clockify widget is using openpype style --- openpype/modules/clockify/widgets.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py index 76f3a3f365..b51ea4df7f 100644 --- a/openpype/modules/clockify/widgets.py +++ b/openpype/modules/clockify/widgets.py @@ -1,6 +1,5 @@ from Qt import QtCore, QtGui, QtWidgets -from avalon import style -from openpype import resources +from openpype import resources, style class MessageWidget(QtWidgets.QWidget): From fbfb85c95d5575d99d14b14e56a6ca7abff65be1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:43:42 +0200 Subject: [PATCH 034/359] simplified clockify widgets --- openpype/modules/clockify/widgets.py | 99 ++++++++++------------------ 1 file changed, 35 insertions(+), 64 deletions(-) diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py index b51ea4df7f..e9f504b851 100644 --- a/openpype/modules/clockify/widgets.py +++ b/openpype/modules/clockify/widgets.py @@ -21,14 +21,6 @@ class MessageWidget(QtWidgets.QWidget): QtCore.Qt.WindowMinimizeButtonHint ) - # Font - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - # Size setting self.resize(self.SIZE_W, self.SIZE_H) self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) @@ -52,7 +44,6 @@ class MessageWidget(QtWidgets.QWidget): labels = [] for message in messages: label = QtWidgets.QLabel(message) - label.setFont(self.font) label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) label.setTextFormat(QtCore.Qt.RichText) label.setWordWrap(True) @@ -102,84 +93,64 @@ class ClockifySettings(QtWidgets.QWidget): icon = QtGui.QIcon(resources.pype_icon_filepath()) self.setWindowIcon(icon) + self.setWindowTitle("Clockify settings") self.setWindowFlags( QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint ) - self._translate = QtCore.QCoreApplication.translate - - # Font - self.font = QtGui.QFont() - self.font.setFamily("DejaVu Sans Condensed") - self.font.setPointSize(9) - self.font.setBold(True) - self.font.setWeight(50) - self.font.setKerning(True) - # Size setting self.resize(self.SIZE_W, self.SIZE_H) self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H)) self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100)) self.setStyleSheet(style.load_stylesheet()) - self.setLayout(self._main()) - self.setWindowTitle('Clockify settings') + self._ui_init() - def _main(self): - self.main = QtWidgets.QVBoxLayout() - self.main.setObjectName("main") + def _ui_init(self): + label_api_key = QtWidgets.QLabel("Clockify API key:") - self.form = QtWidgets.QFormLayout() - self.form.setContentsMargins(10, 15, 10, 5) - self.form.setObjectName("form") + input_api_key = QtWidgets.QLineEdit() + input_api_key.setFrame(True) + input_api_key.setPlaceholderText("e.g. XX1XxXX2x3x4xXxx") - self.label_api_key = QtWidgets.QLabel("Clockify API key:") - self.label_api_key.setFont(self.font) - self.label_api_key.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) - self.label_api_key.setTextFormat(QtCore.Qt.RichText) - self.label_api_key.setObjectName("label_api_key") + error_label = QtWidgets.QLabel("") + error_label.setTextFormat(QtCore.Qt.RichText) + error_label.setWordWrap(True) + error_label.hide() - self.input_api_key = QtWidgets.QLineEdit() - self.input_api_key.setEnabled(True) - self.input_api_key.setFrame(True) - self.input_api_key.setObjectName("input_api_key") - self.input_api_key.setPlaceholderText( - self._translate("main", "e.g. XX1XxXX2x3x4xXxx") - ) + form_layout = QtWidgets.QFormLayout() + form_layout.setContentsMargins(10, 15, 10, 5) + form_layout.addRow(label_api_key, input_api_key) + form_layout.addRow(error_label) - self.error_label = QtWidgets.QLabel("") - self.error_label.setFont(self.font) - self.error_label.setTextFormat(QtCore.Qt.RichText) - self.error_label.setObjectName("error_label") - self.error_label.setWordWrap(True) - self.error_label.hide() + btn_ok = QtWidgets.QPushButton("Ok") + btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer') - self.form.addRow(self.label_api_key, self.input_api_key) - self.form.addRow(self.error_label) - - self.btn_group = QtWidgets.QHBoxLayout() - self.btn_group.addStretch(1) - self.btn_group.setObjectName("btn_group") - - self.btn_ok = QtWidgets.QPushButton("Ok") - self.btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer') - self.btn_ok.clicked.connect(self.click_ok) - - self.btn_cancel = QtWidgets.QPushButton("Cancel") + btn_cancel = QtWidgets.QPushButton("Cancel") cancel_tooltip = 'Application won\'t start' if self.optional: cancel_tooltip = 'Close this window' - self.btn_cancel.setToolTip(cancel_tooltip) - self.btn_cancel.clicked.connect(self._close_widget) + btn_cancel.setToolTip(cancel_tooltip) - self.btn_group.addWidget(self.btn_ok) - self.btn_group.addWidget(self.btn_cancel) + btn_group = QtWidgets.QHBoxLayout() + btn_group.addStretch(1) + btn_group.addWidget(btn_ok) + btn_group.addWidget(btn_cancel) - self.main.addLayout(self.form) - self.main.addLayout(self.btn_group) + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addLayout(form_layout) + main_layout.addLayout(btn_group) - return self.main + btn_ok.clicked.connect(self.click_ok) + btn_cancel.clicked.connect(self._close_widget) + + self.label_api_key = label_api_key + self.input_api_key = input_api_key + self.error_label = error_label + + self.btn_ok = btn_ok + self.btn_cancel = btn_cancel def setError(self, msg): self.error_label.setText(msg) From afb77da237a0a1c30317ed84edf0babb1cf3ea1f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:43:57 +0200 Subject: [PATCH 035/359] buttons in clockify credentials widget has same width --- openpype/modules/clockify/widgets.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py index e9f504b851..fc8e7fa8a3 100644 --- a/openpype/modules/clockify/widgets.py +++ b/openpype/modules/clockify/widgets.py @@ -182,6 +182,17 @@ class ClockifySettings(QtWidgets.QWidget): "Entered invalid API key" ) + def showEvent(self, event): + super(ClockifySettings, self).showEvent(event) + + # Make btns same width + max_width = max( + self.btn_ok.sizeHint().width(), + self.btn_cancel.sizeHint().width() + ) + self.btn_ok.setMinimumWidth(max_width) + self.btn_cancel.setMinimumWidth(max_width) + def closeEvent(self, event): if self.optional is True: event.ignore() From d9259876f6965f88c7ec089ecaccae0288280b01 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 11 Jun 2021 18:44:04 +0200 Subject: [PATCH 036/359] fixed clockify api property --- openpype/modules/clockify/clockify_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/clockify/clockify_api.py b/openpype/modules/clockify/clockify_api.py index 3f0a9799b4..6af911fffc 100644 --- a/openpype/modules/clockify/clockify_api.py +++ b/openpype/modules/clockify/clockify_api.py @@ -36,6 +36,7 @@ class ClockifyAPI: self._secure_registry = None + @property def secure_registry(self): if self._secure_registry is None: self._secure_registry = OpenPypeSecureRegistry("clockify") From 3579f0bd4bada62b6c24f49d0878ab42cb55905c Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 12 Jun 2021 03:46:39 +0000 Subject: [PATCH 037/359] [Automated] Bump version --- CHANGELOG.md | 20 ++++++++++++++++++-- openpype/version.py | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fae98ec11..537be94076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,39 @@ # Changelog -## [3.1.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.1.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...HEAD) #### 🚀 Enhancements +- Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) +- \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683) +- Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682) +- Project Manger: Default name column width [\#1669](https://github.com/pypeclub/OpenPype/pull/1669) +- Remove outline in stylesheet [\#1667](https://github.com/pypeclub/OpenPype/pull/1667) +- TVPaint: Creator take layer name as default value for subset variant [\#1663](https://github.com/pypeclub/OpenPype/pull/1663) +- TVPaint custom subset template [\#1662](https://github.com/pypeclub/OpenPype/pull/1662) +- Feature Slack integration [\#1657](https://github.com/pypeclub/OpenPype/pull/1657) - Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653) +- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650) - \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649) #### 🐛 Bug fixes +- Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) +- Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) +- Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) - Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) -- Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) - Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) - New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) +**Merged pull requests:** + +- Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) +- Add docstrings to Project manager tool [\#1556](https://github.com/pypeclub/OpenPype/pull/1556) + # Changelog diff --git a/openpype/version.py b/openpype/version.py index d6d6a4544b..bf261d41b2 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.1.0-nightly.2" +__version__ = "3.1.0-nightly.3" From 5208583d8fc9026930f186306ebafb3332ad1081 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 12 Jun 2021 12:29:41 +0200 Subject: [PATCH 038/359] reversed logic of drawing in drawTrigon --- .../widgets/color_widgets/color_triangle.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py index d4db175d84..ed7fefa7e8 100644 --- a/openpype/widgets/color_widgets/color_triangle.py +++ b/openpype/widgets/color_widgets/color_triangle.py @@ -724,27 +724,29 @@ class QtColorTriangle(QtWidgets.QWidget): lx = leftX[y] rx = rightX[y] + # if the xdist is 0, don't draw anything. + xdist = rx - lx + if xdist == 0.0: + continue + lxi = int(floor(lx)) rxi = int(floor(rx)) rc = rightColors[y] lc = leftColors[y] - # if the xdist is 0, don't draw anything. - xdist = rx - lx - if xdist != 0.0: - r = lc.r - g = lc.g - b = lc.b - rdelta = (rc.r - r) / xdist - gdelta = (rc.g - g) / xdist - bdelta = (rc.b - b) / xdist + r = lc.r + g = lc.g + b = lc.b + rdelta = (rc.r - r) / xdist + gdelta = (rc.g - g) / xdist + bdelta = (rc.b - b) / xdist - # Inner loop 2. Draws the line from left to right. - for x in range(lxi, rxi + 1): - buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) - r += rdelta - g += gdelta - b += bdelta + # Inner loop 2. Draws the line from left to right. + for x in range(lxi, rxi): + buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) + r += rdelta + g += gdelta + b += bdelta def _radius_at(self, pos, rect): mousexdist = pos.x() - float(rect.center().x()) From fbea4e4f9d0504d4d4af8a7a3e91fbfc58143f0c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 12 Jun 2021 12:29:56 +0200 Subject: [PATCH 039/359] draw 2 more pixels on each side of trigon --- openpype/widgets/color_widgets/color_triangle.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py index ed7fefa7e8..a6ee0e864f 100644 --- a/openpype/widgets/color_widgets/color_triangle.py +++ b/openpype/widgets/color_widgets/color_triangle.py @@ -741,6 +741,10 @@ class QtColorTriangle(QtWidgets.QWidget): gdelta = (rc.g - g) / xdist bdelta = (rc.b - b) / xdist + # Draw 2 more pixels on left side for smoothing + for x in range(lxi - 2, lxi): + buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) + # Inner loop 2. Draws the line from left to right. for x in range(lxi, rxi): buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) @@ -748,6 +752,10 @@ class QtColorTriangle(QtWidgets.QWidget): g += gdelta b += bdelta + # Draw 2 more pixels on right side for smoothing + for x in range(rxi, rxi + 3): + buf.setPixel(x, y, QtGui.qRgb(int(r), int(g), int(b))) + def _radius_at(self, pos, rect): mousexdist = pos.x() - float(rect.center().x()) mouseydist = pos.y() - float(rect.center().y()) From c9b30f276850bf12189c01a7ffdb49a5fe957b29 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sat, 12 Jun 2021 12:30:39 +0200 Subject: [PATCH 040/359] draw trigon to different image which is painter over background with clipping --- .../widgets/color_widgets/color_triangle.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/openpype/widgets/color_widgets/color_triangle.py b/openpype/widgets/color_widgets/color_triangle.py index a6ee0e864f..f4a86c4fa5 100644 --- a/openpype/widgets/color_widgets/color_triangle.py +++ b/openpype/widgets/color_widgets/color_triangle.py @@ -241,7 +241,11 @@ class QtColorTriangle(QtWidgets.QWidget): # Blit the static generated background with the hue gradient onto # the double buffer. - buf = QtGui.QImage(self.bg_image.copy()) + buf = QtGui.QImage( + self.bg_image.width(), + self.bg_image.height(), + QtGui.QImage.Format_RGB32 + ) # Draw the trigon # Find the color with only the hue, and max value and saturation @@ -254,9 +258,21 @@ class QtColorTriangle(QtWidgets.QWidget): ) # Slow step: convert the image to a pixmap - pix = QtGui.QPixmap.fromImage(buf) + pix = self.bg_image.copy() pix_painter = QtGui.QPainter(pix) - pix_painter.setRenderHint(QtGui.QPainter.Antialiasing) + + pix_painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) + + trigon_path = QtGui.QPainterPath() + trigon_path.moveTo(self.point_a) + trigon_path.lineTo(self.point_b) + trigon_path.lineTo(self.point_c) + trigon_path.closeSubpath() + pix_painter.setClipPath(trigon_path) + + pix_painter.drawImage(0, 0, buf) + + pix_painter.setClipping(False) # Draw an outline of the triangle pix_painter.setPen(self._triangle_outline_pen) From 4acca0924c9fc8f9b491a11d5d516ba545ef0a90 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 14 Jun 2021 11:20:30 +0200 Subject: [PATCH 041/359] Global: fix default empty --- openpype/plugins/publish/extract_otio_trimming_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_otio_trimming_video.py b/openpype/plugins/publish/extract_otio_trimming_video.py index 5bcda1dfcc..fdb7c4b096 100644 --- a/openpype/plugins/publish/extract_otio_trimming_video.py +++ b/openpype/plugins/publish/extract_otio_trimming_video.py @@ -30,7 +30,7 @@ class ExtractOTIOTrimmingVideo(openpype.api.Extractor): # get corresponding representation for _repre in representations: - if "trim" not in _repre.get("tags"): + if "trim" not in _repre.get("tags", []): continue input_file = _repre["files"] From bfce09602068eeb4fcb0254b806d90e1d7c439a2 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 14 Jun 2021 12:17:52 +0200 Subject: [PATCH 042/359] update dependencies and add pyenv meta to gitignore --- .gitignore | 3 +- poetry.lock | 150 +++++++++++++++++++++++++++---------------------- pyproject.toml | 2 + 3 files changed, 88 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 754e3698e2..221a2f2241 100644 --- a/.gitignore +++ b/.gitignore @@ -97,4 +97,5 @@ website/.docusaurus # Poetry ######## -.poetry/ \ No newline at end of file +.poetry/ +.python-version diff --git a/poetry.lock b/poetry.lock index 0951427449..48e6f95469 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ develop = false type = "git" url = "https://github.com/pypeclub/acre.git" reference = "master" -resolved_reference = "9bf19573acb9328a3e5f5de96c619060177795cf" +resolved_reference = "efc1b8faa8f84568538b936688ae6f7604dd194c" [[package]] name = "aiohttp" @@ -307,7 +307,7 @@ trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] [[package]] name = "docutils" -version = "0.17.1" +version = "0.16" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false @@ -398,7 +398,7 @@ typing-extensions = {version = ">=3.7.4.0", markers = "python_version < \"3.8\"" [[package]] name = "google-api-core" -version = "1.29.0" +version = "1.30.0" description = "Google API client core library" category = "main" optional = false @@ -436,7 +436,7 @@ uritemplate = ">=3.0.0,<4dev" [[package]] name = "google-auth" -version = "1.30.1" +version = "1.31.0" description = "Google Authentication Library" category = "main" optional = false @@ -449,7 +449,7 @@ rsa = {version = ">=3.1.4,<5", markers = "python_version >= \"3.6\""} six = ">=1.9.0" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"] +aiohttp = ["requests (>=2.20.0,<3.0.0dev)", "aiohttp (>=3.6.2,<4.0.0dev)"] pyopenssl = ["pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] @@ -509,7 +509,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.4.0" +version = "4.5.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -759,7 +759,7 @@ python-versions = "*" [[package]] name = "protobuf" -version = "3.17.2" +version = "3.17.3" description = "Protocol Buffers" category = "main" optional = false @@ -904,7 +904,7 @@ six = "*" [[package]] name = "pyobjc-core" -version = "7.2" +version = "7.3" description = "Python<->ObjC Interoperability Module" category = "main" optional = false @@ -912,26 +912,26 @@ python-versions = ">=3.6" [[package]] name = "pyobjc-framework-cocoa" -version = "7.2" +version = "7.3" description = "Wrappers for the Cocoa frameworks on macOS" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyobjc-core = ">=7.2" +pyobjc-core = ">=7.3" [[package]] name = "pyobjc-framework-quartz" -version = "7.2" +version = "7.3" description = "Wrappers for the Quartz frameworks on macOS" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyobjc-core = ">=7.2" -pyobjc-framework-Cocoa = ">=7.2" +pyobjc-core = ">=7.3" +pyobjc-framework-Cocoa = ">=7.3" [[package]] name = "pyparsing" @@ -1161,6 +1161,18 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "slack-sdk" +version = "3.6.0" +description = "The Slack API Platform SDK for Python" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.extras] +optional = ["aiodns (>1.0)", "aiohttp (>=3.7.3,<4)", "boto3 (<=2)", "SQLAlchemy (>=1,<2)", "websockets (>=9.1,<10)", "websocket-client (>=0.57,<1)"] +testing = ["pytest (>=5.4,<6)", "pytest-asyncio (<1)", "Flask-Sockets (>=0.2,<1)", "pytest-cov (>=2,<3)", "codecov (>=2,<3)", "flake8 (>=3,<4)", "black (==21.5b1)", "psutil (>=5,<6)", "databases (>=0.3)"] + [[package]] name = "smmap" version = "4.0.0" @@ -1230,13 +1242,14 @@ sphinx = "*" [[package]] name = "sphinx-rtd-theme" -version = "0.5.1" +version = "0.5.2" description = "Read the Docs theme for Sphinx" category = "dev" optional = false python-versions = "*" [package.dependencies] +docutils = "<0.17" sphinx = "*" [package.extras] @@ -1453,7 +1466,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "3.7.*" -content-hash = "abfb3fda5422d9119fb942e66d82fad5c7fae46d0ed240876c3a61333e15febe" +content-hash = "8875d530ae66f9763b5b0cb84d9d35edc184ef5c141b63d38bf1ff5a1226e556" [metadata.files] acre = [] @@ -1714,8 +1727,8 @@ dnspython = [ {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, ] docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] enlighten = [ {file = "enlighten-1.10.1-py2.py3-none-any.whl", hash = "sha256:3d6c3eec8cf3eb626ee7b65eddc1b3e904d01f4547a2b9fe7f1da8892a0297e8"}, @@ -1744,16 +1757,16 @@ gitpython = [ {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"}, ] google-api-core = [ - {file = "google-api-core-1.29.0.tar.gz", hash = "sha256:dbe885011111a9afd9ea9a347db6a9c608e82e5c7648c1f7fabf2b4079a579b5"}, - {file = "google_api_core-1.29.0-py2.py3-none-any.whl", hash = "sha256:f83118319d2a28806ec3399671cbe4af5351bd0dac54c40a0395da6162ebe324"}, + {file = "google-api-core-1.30.0.tar.gz", hash = "sha256:0724d354d394b3d763bc10dfee05807813c5210f0bd9b8e2ddf6b6925603411c"}, + {file = "google_api_core-1.30.0-py2.py3-none-any.whl", hash = "sha256:92cd9e9f366e84bfcf2524e34d2dc244906c645e731962617ba620da1620a1e0"}, ] google-api-python-client = [ {file = "google-api-python-client-1.12.8.tar.gz", hash = "sha256:f3b9684442eec2cfe9f9bb48e796ef919456b82142c7528c5fd527e5224f08bb"}, {file = "google_api_python_client-1.12.8-py2.py3-none-any.whl", hash = "sha256:3c4c4ca46b5c21196bec7ee93453443e477d82cbfa79234d1ce0645f81170eaf"}, ] google-auth = [ - {file = "google-auth-1.30.1.tar.gz", hash = "sha256:044d81b1e58012f8ebc71cc134e191c1fa312f543f1fbc99973afe28c25e3228"}, - {file = "google_auth-1.30.1-py2.py3-none-any.whl", hash = "sha256:b3ca7a8ff9ab3bdefee3ad5aefb11fc6485423767eee016f5942d8e606ca23fb"}, + {file = "google-auth-1.31.0.tar.gz", hash = "sha256:154f7889c5d679a6f626f36adb12afbd4dbb0a9a04ec575d989d6ba79c4fd65e"}, + {file = "google_auth-1.31.0-py2.py3-none-any.whl", hash = "sha256:6d47c79b5d09fbc7e8355fd9594cc4cf65fdde5d401c63951eaac4baa1ba2ae1"}, ] google-auth-httplib2 = [ {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, @@ -1776,8 +1789,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.4.0-py3-none-any.whl", hash = "sha256:960d52ba7c21377c990412aca380bf3642d734c2eaab78a2c39319f67c6a5786"}, - {file = "importlib_metadata-4.4.0.tar.gz", hash = "sha256:e592faad8de1bda9fe920cf41e15261e7131bcf266c30306eec00e8e225c1dd5"}, + {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, + {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -1963,6 +1976,7 @@ pillow = [ {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"}, {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"}, {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8b56553c0345ad6dcb2e9b433ae47d67f95fc23fe28a0bde15a120f25257e291"}, {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"}, ] pluggy = [ @@ -1974,29 +1988,29 @@ prefixed = [ {file = "prefixed-0.3.2.tar.gz", hash = "sha256:ca48277ba5fa8346dd4b760847da930c7b84416387c39e93affef086add2c029"}, ] protobuf = [ - {file = "protobuf-3.17.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c49e673436e24e925022c090a98905cfe88d056cb5dde67a8e20ae339acd8140"}, - {file = "protobuf-3.17.2-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4e62be28dcaab52c7e8e8e3fb9a7005dec6374e698f3b22d79276d95c13151e5"}, - {file = "protobuf-3.17.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:c2038dec269b65683f16057886c09ca7526471793029bdd43259282e1e0fb668"}, - {file = "protobuf-3.17.2-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b667ddbb77ff619fdbd18be75445d4448ee68493c9bddd1b4d0b177025e2f6f6"}, - {file = "protobuf-3.17.2-cp35-cp35m-win32.whl", hash = "sha256:993814b0199f22523a227696a8e20d2cd4b9cda60c76d2fb3969ce0c77eb9e0f"}, - {file = "protobuf-3.17.2-cp35-cp35m-win_amd64.whl", hash = "sha256:38b2c6e2204731cfebdb2452ffb7addf0367172b35cff8ccda338ccea9e7c87a"}, - {file = "protobuf-3.17.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:42239d47f213f1ce12ddca62c0c70856efbac09797d715e5d606b3fbc3f16c44"}, - {file = "protobuf-3.17.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4da073b6c1a83e4ff1294156844f21343c5094e295c194d8ecc94703c7a6f42a"}, - {file = "protobuf-3.17.2-cp36-cp36m-win32.whl", hash = "sha256:c303ce92c60d069237cfbd41790fde3d00066ca9500fac464454e20c2f12250c"}, - {file = "protobuf-3.17.2-cp36-cp36m-win_amd64.whl", hash = "sha256:751d71dc6559dd794d309811d8dcf179d6a128b38a1655ae7137530f33895cb4"}, - {file = "protobuf-3.17.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f3f057ad59cd4d5ea2b1bb88d36c6f009b8043007cf03d96cbd3b9c06859d4fa"}, - {file = "protobuf-3.17.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c5b512c1982f8b427a302db094cf79f8f235284014d01b23fba461aa2c1459a0"}, - {file = "protobuf-3.17.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9dc01ddc3b195c4538942790c4b195cf17b52d3785a60c1f6f25b794f51e2071"}, - {file = "protobuf-3.17.2-cp37-cp37m-win32.whl", hash = "sha256:45cc0197e9f9192693c8a4dbcba8c9227a53a2720fc3826dfe791113d9ff5e5e"}, - {file = "protobuf-3.17.2-cp37-cp37m-win_amd64.whl", hash = "sha256:816fe5e8b73c29adb13a57e1653da15f24cbb90e86ea92e6f08abe6d8c0cbdb4"}, - {file = "protobuf-3.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:557e16884d7276caf92c1fb188fe6dfbec47891d3507d4982db1e3d89ffd22de"}, - {file = "protobuf-3.17.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:89613ec119fdcad992f65d7a5bfe3170c17edc839982d0089fc0c9242fb8e517"}, - {file = "protobuf-3.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:744f5c9a3b9e7538c4c70f2b0e46f86858b684f5d17bf78643a19a6c21c7936f"}, - {file = "protobuf-3.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1e08da38d56b74962d9cb05d86ca5f25d2e5b78f05fd00db7900cad3faa6de00"}, - {file = "protobuf-3.17.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:6388e7f300010ea7ac77113c7491c5622645d2447fdf701cbfe026b832d728cd"}, - {file = "protobuf-3.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5a73fa43efda0df0191c162680ec40cef45463fa8ff69fbeaeeddda4c760f5"}, - {file = "protobuf-3.17.2-py2.py3-none-any.whl", hash = "sha256:50c657a54592c1bec7b24521fdbbbd2f7b51325ba23ab505ed03e8ebf3a5aeff"}, - {file = "protobuf-3.17.2.tar.gz", hash = "sha256:5a3450acf046716e4a4f02a3f7adfb7b86f1b5b3ae392cec759915e79538d40d"}, + {file = "protobuf-3.17.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8"}, + {file = "protobuf-3.17.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637"}, + {file = "protobuf-3.17.3-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:1556a1049ccec58c7855a78d27e5c6e70e95103b32de9142bae0576e9200a1b0"}, + {file = "protobuf-3.17.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62"}, + {file = "protobuf-3.17.3-cp35-cp35m-win32.whl", hash = "sha256:a981222367fb4210a10a929ad5983ae93bd5a050a0824fc35d6371c07b78caf6"}, + {file = "protobuf-3.17.3-cp35-cp35m-win_amd64.whl", hash = "sha256:6d847c59963c03fd7a0cd7c488cadfa10cda4fff34d8bc8cba92935a91b7a037"}, + {file = "protobuf-3.17.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:145ce0af55c4259ca74993ddab3479c78af064002ec8227beb3d944405123c71"}, + {file = "protobuf-3.17.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ce4d8bf0321e7b2d4395e253f8002a1a5ffbcfd7bcc0a6ba46712c07d47d0b4"}, + {file = "protobuf-3.17.3-cp36-cp36m-win32.whl", hash = "sha256:7a4c97961e9e5b03a56f9a6c82742ed55375c4a25f2692b625d4087d02ed31b9"}, + {file = "protobuf-3.17.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a22b3a0dbac6544dacbafd4c5f6a29e389a50e3b193e2c70dae6bbf7930f651d"}, + {file = "protobuf-3.17.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b"}, + {file = "protobuf-3.17.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9b7a5c1022e0fa0dbde7fd03682d07d14624ad870ae52054849d8960f04bc764"}, + {file = "protobuf-3.17.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8727ee027157516e2c311f218ebf2260a18088ffb2d29473e82add217d196b1c"}, + {file = "protobuf-3.17.3-cp37-cp37m-win32.whl", hash = "sha256:14c1c9377a7ffbeaccd4722ab0aa900091f52b516ad89c4b0c3bb0a4af903ba5"}, + {file = "protobuf-3.17.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683"}, + {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, + {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, + {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, + {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, + {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, + {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, + {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, + {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, @@ -2136,28 +2150,28 @@ pynput = [ {file = "pynput-1.7.3.tar.gz", hash = "sha256:4e50b1a0ab86847e87e58f6d1993688b9a44f9f4c88d4712315ea8eb552ef828"}, ] pyobjc-core = [ - {file = "pyobjc-core-7.2.tar.gz", hash = "sha256:9e9ec482d80ea030cdb1613d05a247f31eedabe6666d884d42dd890cc5fb0e05"}, - {file = "pyobjc_core-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:94b4d9de9d228db52dd35012096d63bdf8c1ace58ea3be1d5f6f39313cd502f2"}, - {file = "pyobjc_core-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:971cbd7189ae1aa03ef0d16124aa5bcd053779e0e6b6011a41c3dbd5b4ea7e88"}, - {file = "pyobjc_core-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9d93b20394008373d6d2856d49aaff26f4b97ff42d924a14516c8a82313ec8c0"}, - {file = "pyobjc_core-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:860183540d1be792c26426018139ac8ba75e85f675c59ba080ccdc52d8e74c7a"}, - {file = "pyobjc_core-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ffe61d3c2a404354daf2d895e34e38c5044453353581b3c396bf5365de26250c"}, + {file = "pyobjc-core-7.3.tar.gz", hash = "sha256:5081aedf8bb40aac1a8ad95adac9e44e148a882686ded614adf46bb67fd67574"}, + {file = "pyobjc_core-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e93ad769a20b908778fe950f62a843a6d8f0fa71996e5f3cc9fab5ae7d17771"}, + {file = "pyobjc_core-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:9f63fd37bbf3785af4ddb2f86cad5ca81c62cfc7d1c0099637ca18343c3656c1"}, + {file = "pyobjc_core-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9b1311f72f2e170742a7ee3a8149f52c35158dc024a21e88d6f1e52ba5d718b"}, + {file = "pyobjc_core-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d5e12a0729dfd1d998a861998b422d0a3e41923d75ea229bacf31372c831d7b"}, + {file = "pyobjc_core-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:efdee8c4884405e0c0186c57f87d7bfaa0abc1f50b18e865db3caea3a1f329b9"}, ] pyobjc-framework-cocoa = [ - {file = "pyobjc-framework-Cocoa-7.2.tar.gz", hash = "sha256:c8b23f03dc3f4436d36c0fd006a8a084835c4f6015187df7c3aa5de8ecd5c653"}, - {file = "pyobjc_framework_Cocoa-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8e5dd5daa0096755937ec24c345a4b07c3fa131a457f99e0fdeeb01979178ec7"}, - {file = "pyobjc_framework_Cocoa-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:828d183947fc7746953fd0c9b1092cc423745ba0b49719e7b7d1e1614aaa20ec"}, - {file = "pyobjc_framework_Cocoa-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e4c6d7baa0c2ab5ea5efb8836ad0b3b3976cffcfc6195c1f195e826c6eb5744"}, - {file = "pyobjc_framework_Cocoa-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9a9d1d49cc5a810773c88d6de821e60c8cc41d01113cf1b9e7662938f5f7d66"}, - {file = "pyobjc_framework_Cocoa-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:506c2cd09f421eac92b9008a0142174c3d1d70ecd4b0e3fa2b924767995fd14e"}, + {file = "pyobjc-framework-Cocoa-7.3.tar.gz", hash = "sha256:b18d05e7a795a3455ad191c3e43d6bfa673c2a4fd480bb1ccf57191051b80b7e"}, + {file = "pyobjc_framework_Cocoa-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9edffdfa6dd1f71f21b531c3e61fdd3e4d5d3bf6c5a528c98e88828cd60bac11"}, + {file = "pyobjc_framework_Cocoa-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:35a6340437a4e0109a302150b7d1f6baf57004ccf74834f9e6062fcafe2fd8d7"}, + {file = "pyobjc_framework_Cocoa-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7c3886f2608ab3ed02482f8b2ebf9f782b324c559e84b52cfd92dba8a1109872"}, + {file = "pyobjc_framework_Cocoa-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e8e7a1a82cca21d9bfac9115baf065305f3da577bf240085964dfb9c9fff337"}, + {file = "pyobjc_framework_Cocoa-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6c15f43077c9a2ba1853eb402ff7a9515df9e584315bc2fcb779d4c95ef46dc5"}, ] pyobjc-framework-quartz = [ - {file = "pyobjc-framework-Quartz-7.2.tar.gz", hash = "sha256:ea554e5697bc6747a4ce793c0b0036da16622b44ff75196d6124603008922afa"}, - {file = "pyobjc_framework_Quartz-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dc61fe61d26f797e4335f3ffc891bcef64624c728c2603e3307b3910580b2cb8"}, - {file = "pyobjc_framework_Quartz-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ad8103cc38923f2708904db11a0992ea960125ce6adf7b4c7a77d8fdafd412c4"}, - {file = "pyobjc_framework_Quartz-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4549d17ca41f0bf62792d5bc4b4293ba9a6cc560014b3e18ba22c65e4a5030d2"}, - {file = "pyobjc_framework_Quartz-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:da16e4f1e13cb7b02e30fa538cbb3a356e4a694bbc2bb26d2bd100ca12a54ff6"}, - {file = "pyobjc_framework_Quartz-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1f6471177a39535cd0358ae29b8f3d31fe778a21deb74105c448c4e726619d7"}, + {file = "pyobjc-framework-Quartz-7.3.tar.gz", hash = "sha256:98812844c34262def980bdf60923a875cd43428a8375b6fd53bd2cd800eccf0b"}, + {file = "pyobjc_framework_Quartz-7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ef18f5a16511ded65980bf4f5983ea5d35c88224dbad1b3112abd29c60413ea"}, + {file = "pyobjc_framework_Quartz-7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b41eec8d4b10c7c7e011e2f9051367f5499ef315ba52dfbae573c3a2e05469c"}, + {file = "pyobjc_framework_Quartz-7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c65456ed045dfe1711d0298734e5a3ad670f8c770f7eb3b19979256c388bdd2"}, + {file = "pyobjc_framework_Quartz-7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ddbca6b466584c3dc0e5b701b1c2a9b5d97ddc1d79a949927499ebb1be1f210"}, + {file = "pyobjc_framework_Quartz-7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7aba3cd966a0768dd58a35680742820f0c5ac596a9cd11014e2057818e65b0af"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -2269,6 +2283,10 @@ six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +slack-sdk = [ + {file = "slack_sdk-3.6.0-py2.py3-none-any.whl", hash = "sha256:e1b257923a1ef88b8620dd3abff94dc5b3eee16ef37975d101ba9e60123ac3af"}, + {file = "slack_sdk-3.6.0.tar.gz", hash = "sha256:195f044e02a2844579a7a26818ce323e85dde8de224730c859644918d793399e"}, +] smmap = [ {file = "smmap-4.0.0-py2.py3-none-any.whl", hash = "sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"}, {file = "smmap-4.0.0.tar.gz", hash = "sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182"}, @@ -2290,8 +2308,8 @@ sphinx-qt-documentation = [ {file = "sphinx_qt_documentation-0.3.tar.gz", hash = "sha256:f09a0c9d9e989172ba3e282b92bf55613bb23ad47315ec5b0d38536b343ac6c8"}, ] sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl", hash = "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"}, - {file = "sphinx_rtd_theme-0.5.1.tar.gz", hash = "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5"}, + {file = "sphinx_rtd_theme-0.5.2-py2.py3-none-any.whl", hash = "sha256:4a05bdbe8b1446d77a01e20a23ebc6777c74f43237035e76be89699308987d6f"}, + {file = "sphinx_rtd_theme-0.5.2.tar.gz", hash = "sha256:32bd3b5d13dc8186d7a42fc816a23d32e83a4827d7d9882948e7b837c232da5a"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, diff --git a/pyproject.toml b/pyproject.toml index 1425b88236..e376986606 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,8 @@ sphinx-qt-documentation = "*" recommonmark = "*" wheel = "*" enlighten = "*" # cool terminal progress bars +toml = "^0.10.2" # for parsing pyproject.toml + [tool.poetry.urls] "Bug Tracker" = "https://github.com/pypeclub/openpype/issues" From 1e502773fdaf9c66a27f5a251bf46768da67b4d9 Mon Sep 17 00:00:00 2001 From: jezscha Date: Mon, 14 Jun 2021 10:31:57 +0000 Subject: [PATCH 043/359] Create draft PR for #1698 From ac219c1535675f54c98850a3d73380b86489ba5f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:00:27 +0200 Subject: [PATCH 044/359] color entity can have defined if can use alpha --- openpype/settings/entities/color_entity.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index 7a1b1d9848..dfaa75e761 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -12,6 +12,17 @@ class ColorEntity(InputEntity): def _item_initalization(self): self.valid_value_types = (list, ) self.value_on_not_set = [0, 0, 0, 255] + self.use_alpha = self.schema_data.get("use_alpha", True) + + def set_override_state(self, *args, **kwargs): + super(ColorEntity, self).set_override_state(*args, **kwargs) + value = self._current_value + if ( + not self.use_alpha + and isinstance(value, list) + and len(value) == 4 + ): + value[3] = 255 def convert_to_valid_type(self, value): """Conversion to valid type. @@ -51,4 +62,8 @@ class ColorEntity(InputEntity): ).format(value) raise BaseInvalidValueType(reason, self.path) new_value.append(item) + + # Make sure + if not self.use_alpha: + new_value[3] = 255 return new_value From 0c99c730c775582c5813fffa3507dde53d737d0e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:01:11 +0200 Subject: [PATCH 045/359] color widget can show color dialog without alpha based on `use_alpha` attribute --- .../tools/settings/settings/color_widget.py | 8 ++- .../color_widgets/color_picker_widget.py | 55 ++++++++++++------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/openpype/tools/settings/settings/color_widget.py b/openpype/tools/settings/settings/color_widget.py index fa0cd2c989..b38b46f3cb 100644 --- a/openpype/tools/settings/settings/color_widget.py +++ b/openpype/tools/settings/settings/color_widget.py @@ -25,7 +25,9 @@ class ColorWidget(InputWidget): self._dialog.open() return - dialog = ColorDialog(self.input_field.color(), self) + dialog = ColorDialog( + self.input_field.color(), self.entity.use_alpha, self + ) self._dialog = dialog dialog.open() @@ -120,12 +122,12 @@ class ColorViewer(QtWidgets.QWidget): class ColorDialog(QtWidgets.QDialog): - def __init__(self, color=None, parent=None): + def __init__(self, color=None, use_alpha=True, parent=None): super(ColorDialog, self).__init__(parent) self.setWindowTitle("Color picker dialog") - picker_widget = ColorPickerWidget(color, self) + picker_widget = ColorPickerWidget(color, use_alpha, self) footer_widget = QtWidgets.QWidget(self) diff --git a/openpype/widgets/color_widgets/color_picker_widget.py b/openpype/widgets/color_widgets/color_picker_widget.py index 81ec1f87aa..228d35a77c 100644 --- a/openpype/widgets/color_widgets/color_picker_widget.py +++ b/openpype/widgets/color_widgets/color_picker_widget.py @@ -17,19 +17,12 @@ from .color_inputs import ( class ColorPickerWidget(QtWidgets.QWidget): color_changed = QtCore.Signal(QtGui.QColor) - def __init__(self, color=None, parent=None): + def __init__(self, color=None, use_alpha=True, parent=None): super(ColorPickerWidget, self).__init__(parent) # Color triangle color_triangle = QtColorTriangle(self) - alpha_slider_proxy = QtWidgets.QWidget(self) - alpha_slider = AlphaSlider(QtCore.Qt.Horizontal, alpha_slider_proxy) - - alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy) - alpha_slider_layout.setContentsMargins(5, 5, 5, 5) - alpha_slider_layout.addWidget(alpha_slider, 1) - # Eye picked widget pick_widget = PickScreenColorWidget() pick_widget.setMaximumHeight(50) @@ -47,8 +40,6 @@ class ColorPickerWidget(QtWidgets.QWidget): color_view = ColorViewer(self) color_view.setMaximumHeight(50) - alpha_inputs = AlphaInputs(self) - color_inputs_color = QtGui.QColor() col_inputs_by_label = [ ("HEX", HEXInputs(color_inputs_color, self)), @@ -58,6 +49,7 @@ class ColorPickerWidget(QtWidgets.QWidget): ] layout = QtWidgets.QGridLayout(self) + empty_col = 1 label_col = empty_col + 1 input_col = label_col + 1 @@ -65,6 +57,9 @@ class ColorPickerWidget(QtWidgets.QWidget): empty_widget.setFixedWidth(10) layout.addWidget(empty_widget, 0, empty_col) + layout.setColumnStretch(0, 1) + layout.setColumnStretch(input_col, 1) + row = 0 layout.addWidget(btn_pick_color, row, label_col) layout.addWidget(color_view, row, input_col) @@ -84,20 +79,41 @@ class ColorPickerWidget(QtWidgets.QWidget): layout.setRowStretch(row, 1) row += 1 - layout.addWidget(alpha_slider_proxy, row, 0) + alpha_label = None + alpha_slider_proxy = None + alpha_slider = None + alpha_inputs = None + if not use_alpha: + color.setAlpha(255) + else: + alpha_inputs = AlphaInputs(self) + alpha_label = QtWidgets.QLabel("Alpha", self) + alpha_slider_proxy = QtWidgets.QWidget(self) + alpha_slider = AlphaSlider( + QtCore.Qt.Horizontal, alpha_slider_proxy + ) + + alpha_slider_layout = QtWidgets.QHBoxLayout(alpha_slider_proxy) + alpha_slider_layout.setContentsMargins(5, 5, 5, 5) + alpha_slider_layout.addWidget(alpha_slider, 1) + + layout.addWidget(alpha_slider_proxy, row, 0) + + layout.addWidget(alpha_label, row, label_col) + layout.addWidget(alpha_inputs, row, input_col) + + row += 1 - layout.addWidget(QtWidgets.QLabel("Alpha", self), row, label_col) - layout.addWidget(alpha_inputs, row, input_col) - row += 1 layout.setRowStretch(row, 1) color_view.set_color(color_triangle.cur_color) color_triangle.color_changed.connect(self.triangle_color_changed) - alpha_slider.valueChanged.connect(self._on_alpha_slider_change) pick_widget.color_selected.connect(self.on_color_change) - alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed) btn_pick_color.released.connect(self.pick_color) + if alpha_slider: + alpha_slider.valueChanged.connect(self._on_alpha_slider_change) + alpha_inputs.alpha_changed.connect(self._on_alpha_inputs_changed) self.color_input_fields = color_input_fields self.color_inputs_color = color_inputs_color @@ -131,7 +147,8 @@ class ColorPickerWidget(QtWidgets.QWidget): return self.color_view.color() def set_color(self, color): - self.alpha_inputs.set_alpha(color.alpha()) + if self.alpha_inputs: + self.alpha_inputs.set_alpha(color.alpha()) self.on_color_change(color) def pick_color(self): @@ -163,10 +180,10 @@ class ColorPickerWidget(QtWidgets.QWidget): def alpha_changed(self, value): self.color_view.set_alpha(value) - if self.alpha_slider.value() != value: + if self.alpha_slider and self.alpha_slider.value() != value: self.alpha_slider.setValue(value) - if self.alpha_inputs.alpha_value != value: + if self.alpha_inputs and self.alpha_inputs.alpha_value != value: self.alpha_inputs.set_alpha(value) def _on_alpha_inputs_changed(self, value): From f67b496bba879e909407ba2d14536fd6bf7aa035 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:01:30 +0200 Subject: [PATCH 046/359] fix color view piece size --- openpype/widgets/color_widgets/color_view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/widgets/color_widgets/color_view.py b/openpype/widgets/color_widgets/color_view.py index 8644281a1d..b5fce28894 100644 --- a/openpype/widgets/color_widgets/color_view.py +++ b/openpype/widgets/color_widgets/color_view.py @@ -5,6 +5,8 @@ def draw_checkerboard_tile(piece_size=None, color_1=None, color_2=None): if piece_size is None: piece_size = 7 + # Make sure piece size is not float + piece_size = int(piece_size) if color_1 is None: color_1 = QtGui.QColor(188, 188, 188) From c21165643a92e52f2545acb02902d6881b5368f8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:02:55 +0200 Subject: [PATCH 047/359] added overscan_color to extract review settings --- .../settings/defaults/project_settings/global.json | 6 ++++++ .../projects_schema/schemas/schema_global_publish.json | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 4351f18a60..a86b3c712a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -58,6 +58,12 @@ "overscan_crop": "", "width": 0, "height": 0, + "overscan_color": [ + 0, + 0, + 0, + 255 + ], "bg_color": [ 0, 0, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 0c89575d74..9f7a573df9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -202,6 +202,16 @@ "minimum": 0, "maximum": 100000 }, + { + "type": "label", + "label": "Overscan color is used only when output aspect pixel ratio is not same as input ratio." + }, + { + "type": "color", + "label": "Overscan color", + "key": "overscan_color", + "use_alpha": false + }, { "type": "label", "label": "Background color is used only when input have transparency and Alpha is higher than 0." From a733640a7f8c34ac496d236888e2e223da7e0b6d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:07:57 +0200 Subject: [PATCH 048/359] overscan color is used inside extract review if is set --- openpype/plugins/publish/extract_review.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e1e24af3ea..47c5461517 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -1154,13 +1154,24 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("height_scale: `{}`".format(height_scale)) self.log.debug("height_half_pad: `{}`".format(height_half_pad)) + # Overscal color + overscan_color_value = "black" + overscan_color = output_def.get("overscan_color") + if overscan_color: + bg_red, bg_green, bg_blue, _ = overscan_color + overscan_color_value = "#{0:0>2X}{1:0>2X}{2:0>2X}".format( + bg_red, bg_green, bg_blue + ) + self.log.debug("Overscan color: `{}`".format(overscan_color_value)) + filters.extend([ "scale={}x{}:flags=lanczos".format( width_scale, height_scale ), - "pad={}:{}:{}:{}:black".format( + "pad={}:{}:{}:{}:{}".format( output_width, output_height, - width_half_pad, height_half_pad + width_half_pad, height_half_pad, + overscan_color_value ), "setsar=1" ]) From 0183ac73b2783f779d25fb3d59e26dc8798b8d0a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:21:26 +0200 Subject: [PATCH 049/359] scrolling of environments view start when you scroll to bottom of widget --- openpype/tools/tray/pype_info_widget.py | 52 ++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/openpype/tools/tray/pype_info_widget.py b/openpype/tools/tray/pype_info_widget.py index f95a31f7c2..2965463c37 100644 --- a/openpype/tools/tray/pype_info_widget.py +++ b/openpype/tools/tray/pype_info_widget.py @@ -28,6 +28,8 @@ class EnvironmentsView(QtWidgets.QTreeView): def __init__(self, parent=None): super(EnvironmentsView, self).__init__(parent) + self._scroll_enabled = False + model = QtGui.QStandardItemModel() env = os.environ.copy() @@ -112,8 +114,11 @@ class EnvironmentsView(QtWidgets.QTreeView): else: return super(EnvironmentsView, self).keyPressEvent(event) + def set_scroll_enabled(self, value): + self._scroll_enabled = value + def wheelEvent(self, event): - if not self.hasFocus(): + if not self._scroll_enabled: event.ignore() return return super(EnvironmentsView, self).wheelEvent(event) @@ -200,9 +205,13 @@ class CollapsibleWidget(QtWidgets.QWidget): class PypeInfoWidget(QtWidgets.QWidget): + _resized = QtCore.Signal() + def __init__(self, parent=None): super(PypeInfoWidget, self).__init__(parent) + self._scroll_at_bottom = False + self.setStyleSheet(style.load_stylesheet()) icon = QtGui.QIcon(resources.pype_icon_filepath()) @@ -219,11 +228,39 @@ class PypeInfoWidget(QtWidgets.QWidget): main_layout.addWidget(scroll_area, 1) main_layout.addWidget(self._create_btns_section(), 0) + scroll_area.verticalScrollBar().valueChanged.connect( + self._on_area_scroll + ) + self._resized.connect(self._on_resize) self.resize(740, 540) self.scroll_area = scroll_area self.info_widget = info_widget + def _on_area_scroll(self, value): + vertical_bar = self.scroll_area.verticalScrollBar() + self._scroll_at_bottom = vertical_bar.maximum() == vertical_bar.value() + self.info_widget.set_scroll_enabled(self._scroll_at_bottom) + + def _on_resize(self): + if not self._scroll_at_bottom: + return + vertical_bar = self.scroll_area.verticalScrollBar() + vertical_bar.setValue(vertical_bar.maximum()) + + def resizeEvent(self, event): + super(PypeInfoWidget, self).resizeEvent(event) + self._resized.emit() + self.info_widget.set_content_height( + self.scroll_area.height() + ) + + def showEvent(self, event): + super(PypeInfoWidget, self).showEvent(event) + self.info_widget.set_content_height( + self.scroll_area.height() + ) + def _create_btns_section(self): btns_widget = QtWidgets.QWidget(self) btns_layout = QtWidgets.QHBoxLayout(btns_widget) @@ -282,6 +319,8 @@ class PypeInfoSubWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(PypeInfoSubWidget, self).__init__(parent) + self.env_view = None + main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setAlignment(QtCore.Qt.AlignTop) @@ -293,6 +332,14 @@ class PypeInfoSubWidget(QtWidgets.QWidget): main_layout.addWidget(self._create_separator(), 0) main_layout.addWidget(self._create_environ_widget(), 1) + def set_content_height(self, height): + if self.env_view: + self.env_view.setMinimumHeight(height) + + def set_scroll_enabled(self, value): + if self.env_view: + self.env_view.set_scroll_enabled(value) + def _create_separator(self): separator_widget = QtWidgets.QWidget(self) separator_widget.setObjectName("Separator") @@ -369,9 +416,10 @@ class PypeInfoSubWidget(QtWidgets.QWidget): env_view = EnvironmentsView(env_widget) env_view.setMinimumHeight(300) - env_widget.set_content_widget(env_view) + self.env_view = env_view + return env_widget def _create_openpype_info_widget(self): From a96e8b4d4ae6228492488191c1fa2f8e53ad244f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:40:35 +0200 Subject: [PATCH 050/359] log viewer use openpype style --- openpype/modules/log_viewer/tray/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/log_viewer/tray/app.py b/openpype/modules/log_viewer/tray/app.py index c0e180c8a1..9aab37cd20 100644 --- a/openpype/modules/log_viewer/tray/app.py +++ b/openpype/modules/log_viewer/tray/app.py @@ -1,6 +1,6 @@ from Qt import QtWidgets, QtCore from .widgets import LogsWidget, OutputWidget -from avalon import style +from openpype import style class LogsWindow(QtWidgets.QWidget): @@ -14,7 +14,7 @@ class LogsWindow(QtWidgets.QWidget): main_layout = QtWidgets.QHBoxLayout() - log_splitter = QtWidgets.QSplitter() + log_splitter = QtWidgets.QSplitter(self) log_splitter.setOrientation(QtCore.Qt.Horizontal) log_splitter.addWidget(logs_widget) log_splitter.addWidget(log_detail) From 5784d75153da611f4990003354d260aa38d9c113 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:41:02 +0200 Subject: [PATCH 051/359] modified how qtoolbutton looks with menu --- openpype/style/style.css | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index 645e68ce64..c57b9a8da6 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -97,6 +97,29 @@ QToolButton:disabled { background: {color:bg-buttons-disabled}; } +QToolButton[popupMode="1"] { + /* make way for the popup button */ + padding-right: 20px; + border: 1px solid {color:bg-buttons}; +} + +QToolButton::menu-button { + width: 16px; + /* Set border only of left side. */ + border: 1px solid transparent; + border-left: 1px solid {color:bg-buttons}; +} + +QToolButton::menu-arrow { + /* Offset arrow a little bit to center. */ + left: 1px; top: 1px; +} + +QToolButton::menu-arrow:open { + /* Don't offset arrow on open. */ + left: 0px; top: 0px; +} + /* QMenu */ QMenu { border: 1px solid #555555; From fd2a45b7f9a10afc504dde21ec028972fe05713c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:41:17 +0200 Subject: [PATCH 052/359] removed unused lines --- openpype/modules/log_viewer/tray/widgets.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index cd0df283bf..d55307a0d4 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -83,7 +83,6 @@ class CustomCombo(QtWidgets.QWidget): self.setLayout(layout) - # toolmenu.selection_changed.connect(self.on_selection_changed) toolmenu.selection_changed.connect(self.selection_changed) self.toolbutton = toolbutton @@ -119,7 +118,6 @@ class LogsWidget(QtWidgets.QWidget): filter_layout = QtWidgets.QHBoxLayout() - # user_filter = SearchComboBox(self, "Users") user_filter = CustomCombo("Users", self) users = model.dbcon.distinct("username") user_filter.populate(users) @@ -128,7 +126,6 @@ class LogsWidget(QtWidgets.QWidget): proxy_model.update_users_filter(users) level_filter = CustomCombo("Levels", self) - # levels = [(level, True) for level in model.dbcon.distinct("level")] levels = model.dbcon.distinct("level") level_filter.addItems(levels) level_filter.selection_changed.connect(self._level_changed) From 3bf863ea6cfcbd742b5e71dc81ed053bcc212b4e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 15:41:32 +0200 Subject: [PATCH 053/359] replace spacer widget with stretch --- openpype/modules/log_viewer/tray/widgets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py index d55307a0d4..b9a8499a4c 100644 --- a/openpype/modules/log_viewer/tray/widgets.py +++ b/openpype/modules/log_viewer/tray/widgets.py @@ -132,14 +132,12 @@ class LogsWidget(QtWidgets.QWidget): detail_widget.update_level_filter(levels) - spacer = QtWidgets.QWidget() - icon = qtawesome.icon("fa.refresh", color="white") refresh_btn = QtWidgets.QPushButton(icon, "") filter_layout.addWidget(user_filter) filter_layout.addWidget(level_filter) - filter_layout.addWidget(spacer, 1) + filter_layout.addStretch(1) filter_layout.addWidget(refresh_btn) view = QtWidgets.QTreeView(self) From 03c3394859e856fea7e6c9f8caeebe194f203b5b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 19:13:48 +0200 Subject: [PATCH 054/359] fixed ffmpeg arguments in standalone publisher extract thumbnail --- .../plugins/publish/extract_thumbnail.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index ac1dfa13d4..53c81ee32a 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -1,6 +1,5 @@ import os import tempfile -import subprocess import pyblish.api import openpype.api import openpype.lib @@ -77,30 +76,30 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): ffmpeg_args = self.ffmpeg_args or {} - jpeg_items = [] - jpeg_items.append("\"{}\"".format(ffmpeg_path)) - # override file if already exists - jpeg_items.append("-y") + jpeg_items = [ + "\"{}\"".format(ffmpeg_path), + # override file if already exists + "-y" + ] + # add input filters from peresets jpeg_items.extend(ffmpeg_args.get("input") or []) # input file - jpeg_items.append("-i {}".format(full_input_path)) + jpeg_items.append("-i \"{}\"".format(full_input_path)) # extract only single file - jpeg_items.append("-vframes 1") + jpeg_items.append("-frames:v 1") jpeg_items.extend(ffmpeg_args.get("output") or []) # output file - jpeg_items.append(full_thumbnail_path) + jpeg_items.append("\"{}\"".format(full_thumbnail_path)) subprocess_jpeg = " ".join(jpeg_items) # run subprocess self.log.debug("Executing: {}".format(subprocess_jpeg)) - subprocess.Popen( - subprocess_jpeg, - stdout=subprocess.PIPE, - shell=True + openpype.api.run_subprocess( + subprocess_jpeg, shell=True, logger=self.log ) # remove thumbnail key from origin repre From 4cc97d6eebdb28841966b967017800561ac3e90f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 14 Jun 2021 19:14:58 +0200 Subject: [PATCH 055/359] replaced -gamma 2.2 with -apply_trc gamma22 in extract thumbnail --- .../settings/defaults/project_settings/standalonepublisher.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 979f5285d3..7172612a74 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -154,7 +154,7 @@ "ExtractThumbnailSP": { "ffmpeg_args": { "input": [ - "-gamma 2.2" + "-apply_trc gamma22" ], "output": [] } From cc06efc4156bdedd59b4382aec72a5b7dce8a2f2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 14 Jun 2021 23:00:35 +0200 Subject: [PATCH 056/359] Nuke: settings prerender use limit by default --- openpype/settings/defaults/project_settings/nuke.json | 3 ++- .../schemas/projects_schema/schema_project_nuke.json | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 3736f67268..6ff732634e 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -13,7 +13,8 @@ "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}" }, "CreateWritePrerender": { - "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}" + "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}", + "use_range_limit": true } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index f709e84651..01a954f283 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -77,6 +77,11 @@ "type": "text", "key": "fpath_template", "label": "Path template" + }, + { + "type": "boolean", + "key": "use_range_limit", + "label": "Use Frame range limit by default" } ] } From b78f990c3c53ec61d42302ac6e23caeb3e144c95 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 14 Jun 2021 23:01:41 +0200 Subject: [PATCH 057/359] Nuke: render node linked knobs move to lib --- openpype/hosts/nuke/api/lib.py | 40 ++++++++++++------- .../plugins/create/create_write_prerender.py | 20 +++------- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 7c274a03c7..d7f3fdc6ba 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -286,7 +286,8 @@ def add_button_write_to_read(node): node.addKnob(knob) -def create_write_node(name, data, input=None, prenodes=None, review=True): +def create_write_node(name, data, input=None, prenodes=None, + review=True, linked_knobs=None): ''' Creating write node which is group node Arguments: @@ -465,12 +466,16 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): GN.addKnob(nuke.Text_Knob('', 'Rendering')) # Add linked knobs. - linked_knob_names = [ - "_grp-start_", - "use_limit", "first", "last", - "_grp-end_", - "Render" - ] + linked_knob_names = [] + + # add input linked knobs and create group only if any input + if linked_knobs: + linked_knob_names.append("_grp-start_") + linked_knob_names.extend(linked_knobs) + linked_knob_names.append("_grp-end_") + + linked_knob_names.append("Render") + for name in linked_knob_names: if "_grp-start_" in name: knob = nuke.Tab_Knob( @@ -481,13 +486,20 @@ def create_write_node(name, data, input=None, prenodes=None, review=True): "rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP) GN.addKnob(knob) else: - link = nuke.Link_Knob("") - link.makeLink(write_node.name(), name) - link.setName(name) - if "Render" in name: - link.setLabel("Render Local") - link.setFlag(0x1000) - GN.addKnob(link) + if "___" in name: + # add devider + GN.addKnob(nuke.Text_Knob("")) + else: + # add linked knob by name + link = nuke.Link_Knob("") + link.makeLink(write_node.name(), name) + link.setName(name) + + # make render + if "Render" in name: + link.setLabel("Render Local") + link.setFlag(0x1000) + GN.addKnob(link) # adding write to read button add_button_write_to_read(GN) diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 6e1a2ddd96..bb01236801 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -103,7 +103,8 @@ class CreateWritePrerender(plugin.PypeCreator): write_data, input=selected_node, prenodes=[], - review=False) + review=False, + linked_knobs=["channels", "___", "first", "last", "use_limit"]) # relinking to collected connections for i, input in enumerate(inputs): @@ -122,19 +123,10 @@ class CreateWritePrerender(plugin.PypeCreator): w_node = n write_node.end() - # add inner write node Tab - write_node.addKnob(nuke.Tab_Knob("WriteLinkedKnobs")) + if self.presets.get("use_range_limit"): + w_node["use_limit"].setValue(True) + w_node["first"].setValue(nuke.root()["first_frame"].value()) + w_node["last"].setValue(nuke.root()["last_frame"].value()) - # linking knobs to group property panel - linking_knobs = ["channels", "___", "first", "last", "use_limit"] - for k in linking_knobs: - if "___" in k: - write_node.addKnob(nuke.Text_Knob('')) - else: - lnk = nuke.Link_Knob(k) - lnk.makeLink(w_node.name(), k) - lnk.setName(k.replace('_', ' ').capitalize()) - lnk.clearFlag(nuke.STARTLINE) - write_node.addKnob(lnk) return write_node From b34d124607ff1bf1826c262a72650c8da815cd02 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 14 Jun 2021 23:34:09 +0200 Subject: [PATCH 058/359] Nuke: broken publishing rendered frames --- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index cdb0589525..00d96c6cd1 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -81,17 +81,18 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): if target == "Use existing frames": # Local rendering self.log.info("flagged for no render") + families.append(family) elif target == "Local": # Local rendering self.log.info("flagged for local render") families.append("{}.local".format(family)) - family = families_ak.lower() elif target == "On farm": # Farm rendering self.log.info("flagged for farm render") instance.data["transfer"] = False families.append("{}.farm".format(family)) - family = families_ak.lower() + + family = families_ak.lower() node.begin() for i in nuke.allNodes(): From 21bc686a28bff4b07f2039b1e8e570a433b7ce4b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 10:47:28 +0200 Subject: [PATCH 059/359] add black background for transparent images --- .../plugins/publish/extract_thumbnail.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 53c81ee32a..963d47956a 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -88,6 +88,13 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): jpeg_items.append("-i \"{}\"".format(full_input_path)) # extract only single file jpeg_items.append("-frames:v 1") + # Add black background for transparent images + jpeg_items.append(( + "-filter_complex" + " \"color=black,format=rgb24[c]" + ";[c][0]scale2ref[c][i]" + ";[c][i]overlay=format=auto:shortest=1,setsar=1\"" + )) jpeg_items.extend(ffmpeg_args.get("output") or []) From 5692d570b760f7c1de00a320b84f564918b431e5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:08:36 +0200 Subject: [PATCH 060/359] #680 - Toggle Ftrack upload in StandalonePublisher Removed default_family in collect_context --- .../plugins/publish/collect_ftrack_family.py | 68 +++++++++++++------ .../defaults/project_settings/ftrack.json | 31 +++++++++ .../project_settings/standalonepublisher.json | 21 ++++-- .../schema_project_ftrack.json | 53 +++++++++++++++ 4 files changed, 146 insertions(+), 27 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index e6daed9a33..85e9c4ab9e 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -1,29 +1,57 @@ +""" +Requires: + none + +Provides: + instance -> families ([]) +""" +import os import pyblish.api +from openpype.lib.plugin_tools import filter_profiles -class CollectFtrackFamilies(pyblish.api.InstancePlugin): - """Collect family for ftrack publishing - - Add ftrack family to those instance that should be published to ftrack +class CollectFtrackFamily(pyblish.api.InstancePlugin): """ + Adds explicitly 'ftrack' to families to upload instance to FTrack. - order = pyblish.api.CollectorOrder + 0.3 - label = 'Add ftrack family' - families = ["model", - "setdress", - "model", - "animation", - "look", - "rig", - "camera" - ] - hosts = ["maya"] + Uses selection by combination of hosts/families/tasks names via + profiles resolution. + + Triggered everywhere, checks instance against configured + """ + label = "Collect Ftrack Family" + order = pyblish.api.CollectorOrder + 0.4999 + + profiles = None def process(self, instance): + if self.profiles: + anatomy_data = instance.context.data["anatomyData"] + task_name = anatomy_data.get("task", + os.environ["AVALON_TASK"]) + host_name = anatomy_data.get("app", + os.environ["AVALON_APP"]) + family = instance.data["family"] - # make ftrack publishable - if instance.data.get('families'): - instance.data['families'].append('ftrack') - else: - instance.data['families'] = ['ftrack'] + filtering_criteria = { + "hosts": host_name, + "families": family, + "tasks": task_name + } + profile = filter_profiles(self.profiles, filtering_criteria) + + if profile: + families = instance.data.get("families") + if profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] + else: + self.log.debug("Removing ftrack family if present") + if families and "ftrack" in families: + instance.data["families"].pop("ftrack") + + self.log.debug("instance.data:: {}".format(instance.data)) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index b964ce07c3..26361a091a 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -200,6 +200,37 @@ } }, "publish": { + "CollectFtrackFamily": { + "enabled": true, + "profiles": [ + { + "families": [ + "render", + "image" + ], + "tasks": [], + "hosts": [ + "standalonepublisher" + ], + "add_ftrack_family": true + }, + { + "families": [ + "model", + "setdress", + "animation", + "look", + "rig", + "camera" + ], + "tasks": [], + "hosts": [ + "maya" + ], + "add_ftrack_family": true + } + ] + }, "IntegrateFtrackNote": { "enabled": true, "note_with_intent_template": "{intent}: {comment}", diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 979f5285d3..8e833de7e0 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -105,16 +105,23 @@ "label": "Render", "family": "render", "icon": "image", - "defaults": ["Animation", "Lighting", "Lookdev", "Compositing"], + "defaults": [ + "Animation", + "Lighting", + "Lookdev", + "Compositing" + ], "help": "Rendered images or video files" }, "create_mov_batch": { - "name": "mov_batch", - "label": "Batch Mov", - "family": "render_mov_batch", - "icon": "image", - "defaults": ["Main"], - "help": "Process multiple Mov files and publish them for layout and comp." + "name": "mov_batch", + "label": "Batch Mov", + "family": "render_mov_batch", + "icon": "image", + "defaults": [ + "Main" + ], + "help": "Process multiple Mov files and publish them for layout and comp." }, "__dynamic_keys_labels__": { "create_workfile": "Workfile", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index aae2bb2539..af85ccb254 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -604,6 +604,59 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectFtrackFamily", + "label": "Collect Ftrack Family", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "collapsible": true, + "key": "profiles", + "label": "Profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Host names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "add_ftrack_family", + "label": "Add Ftrack Family", + "type": "boolean" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From dad17b9184be8d96c9ca39b9028d7f3e32675ce9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:27:08 +0200 Subject: [PATCH 061/359] #680 - fixed where to check for Task definition Modified default profile for StandalonePublisher --- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 4 ++-- openpype/settings/defaults/project_settings/ftrack.json | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 85e9c4ab9e..a66d4217b5 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -28,8 +28,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): def process(self, instance): if self.profiles: anatomy_data = instance.context.data["anatomyData"] - task_name = anatomy_data.get("task", - os.environ["AVALON_TASK"]) + task_name = instance.data("task", + instance.context.data["task"]) host_name = anatomy_data.get("app", os.environ["AVALON_APP"]) family = instance.data["family"] diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 26361a091a..a8e121d7c3 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -204,10 +204,7 @@ "enabled": true, "profiles": [ { - "families": [ - "render", - "image" - ], + "families": [], "tasks": [], "hosts": [ "standalonepublisher" From 137097631d3f0189c380b94f4e853fb67746c810 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:30:37 +0200 Subject: [PATCH 062/359] #680 - added warning if no profiles configured, changed if order --- .../plugins/publish/collect_ftrack_family.py | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index a66d4217b5..e910970290 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -26,32 +26,30 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): profiles = None def process(self, instance): - if self.profiles: - anatomy_data = instance.context.data["anatomyData"] - task_name = instance.data("task", - instance.context.data["task"]) - host_name = anatomy_data.get("app", - os.environ["AVALON_APP"]) - family = instance.data["family"] + if not self.profiles: + self.log.warning("No profiles present for adding Ftrack family") + return - filtering_criteria = { - "hosts": host_name, - "families": family, - "tasks": task_name - } - profile = filter_profiles(self.profiles, filtering_criteria) + anatomy_data = instance.context.data["anatomyData"] + task_name = instance.data("task", + instance.context.data["task"]) + host_name = anatomy_data.get("app", + os.environ["AVALON_APP"]) + family = instance.data["family"] - if profile: - families = instance.data.get("families") - if profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") - if families and "ftrack" not in families: - instance.data["families"].append("ftrack") - else: - instance.data["families"] = ["ftrack"] - else: - self.log.debug("Removing ftrack family if present") - if families and "ftrack" in families: - instance.data["families"].pop("ftrack") + filtering_criteria = { + "hosts": host_name, + "families": family, + "tasks": task_name + } + profile = filter_profiles(self.profiles, filtering_criteria) + + if profile and profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") + families = instance.data.get("families") + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] self.log.debug("instance.data:: {}".format(instance.data)) From ad59c70a985fb9818866ee6302645d0eae18c2c0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 11:31:07 +0200 Subject: [PATCH 063/359] #680 - removed implicit assignment for SP --- .../standalonepublisher/plugins/publish/collect_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index 43ab13cd79..d4855da140 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -34,7 +34,7 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): # presets batch_extensions = ["edl", "xml", "psd"] - default_families = ["ftrack"] + default_families = [] def process(self, context): # get json paths from os and load them From be02d1582247db69e9b79a68998ac3d10664e4ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 11:57:09 +0200 Subject: [PATCH 064/359] overscan color is also used in overscan crop --- openpype/plugins/publish/extract_review.py | 41 ++++++++++++++-------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 47c5461517..42fb2a8f93 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -986,9 +986,21 @@ class ExtractReview(pyblish.api.InstancePlugin): output_width = output_def.get("width") or None output_height = output_def.get("height") or None + # Overscal color + overscan_color_value = "black" + overscan_color = output_def.get("overscan_color") + if overscan_color: + bg_red, bg_green, bg_blue, _ = overscan_color + overscan_color_value = "#{0:0>2X}{1:0>2X}{2:0>2X}".format( + bg_red, bg_green, bg_blue + ) + self.log.debug("Overscan color: `{}`".format(overscan_color_value)) + # Convert overscan value video filters overscan_crop = output_def.get("overscan_crop") - overscan = OverscanCrop(input_width, input_height, overscan_crop) + overscan = OverscanCrop( + input_width, input_height, overscan_crop, overscan_color_value + ) overscan_crop_filters = overscan.video_filters() # Add overscan filters to filters if are any and modify input # resolution by it's values @@ -1154,16 +1166,6 @@ class ExtractReview(pyblish.api.InstancePlugin): self.log.debug("height_scale: `{}`".format(height_scale)) self.log.debug("height_half_pad: `{}`".format(height_half_pad)) - # Overscal color - overscan_color_value = "black" - overscan_color = output_def.get("overscan_color") - if overscan_color: - bg_red, bg_green, bg_blue, _ = overscan_color - overscan_color_value = "#{0:0>2X}{1:0>2X}{2:0>2X}".format( - bg_red, bg_green, bg_blue - ) - self.log.debug("Overscan color: `{}`".format(overscan_color_value)) - filters.extend([ "scale={}x{}:flags=lanczos".format( width_scale, height_scale @@ -1718,12 +1720,15 @@ class OverscanCrop: item_regex = re.compile(r"([\+\-])?([0-9]+)(.+)?") relative_source_regex = re.compile(r"%([\+\-])") - def __init__(self, input_width, input_height, string_value): + def __init__( + self, input_width, input_height, string_value, overscal_color=None + ): # Make sure that is not None string_value = string_value or "" self.input_width = input_width self.input_height = input_height + self.overscal_color = overscal_color width, height = self._convert_string_to_values(string_value) self._width_value = width @@ -1778,16 +1783,22 @@ class OverscanCrop: elif width >= self.input_width and height >= self.input_height: output.append( - "pad={}:{}:(iw-ow)/2:(ih-oh)/2".format(width, height) + "pad={}:{}:(iw-ow)/2:(ih-oh)/2:{}".format( + width, height, self.overscal_color + ) ) elif width > self.input_width and height < self.input_height: output.append("crop=iw:{}".format(height)) - output.append("pad={}:ih:(iw-ow)/2:(ih-oh)/2".format(width)) + output.append("pad={}:ih:(iw-ow)/2:(ih-oh)/2:{}".format( + width, self.overscal_color + )) elif width < self.input_width and height > self.input_height: output.append("crop={}:ih".format(width)) - output.append("pad=iw:{}:(iw-ow)/2:(ih-oh)/2".format(height)) + output.append("pad=iw:{}:(iw-ow)/2:(ih-oh)/2:{}".format( + height, self.overscal_color + )) return output From 5ea17137495657c7f32c73359c890c252841ab92 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 12:00:00 +0200 Subject: [PATCH 065/359] #680 - changed logging Added explicit removing of ftrack family if configured to remove --- .../plugins/publish/collect_ftrack_family.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index e910970290..a66e6db29f 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -44,12 +44,19 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): } profile = filter_profiles(self.profiles, filtering_criteria) - if profile and profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") + if profile: families = instance.data.get("families") - if families and "ftrack" not in families: - instance.data["families"].append("ftrack") - else: - instance.data["families"] = ["ftrack"] + if profile["add_ftrack_family"]: + self.log.debug("Adding ftrack family") - self.log.debug("instance.data:: {}".format(instance.data)) + if families and "ftrack" not in families: + instance.data["families"].append("ftrack") + else: + instance.data["families"] = ["ftrack"] + else: + if families and "ftrack" in families: + self.log.debug("Explicitly removing 'ftrack'") + instance.data["families"].remove("ftrack") + + self.log.debug("Resulting families '{}' for '{}'".format( + instance.data["families"], instance.data["family"])) From 8b7326e9ec43e8a7cf7ca4a67bfa4bb1475dafb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 12:21:33 +0200 Subject: [PATCH 066/359] changed position of overscan color --- .../defaults/project_settings/global.json | 4 ++-- .../schemas/schema_global_publish.json | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index a86b3c712a..b7fa5e32e8 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -56,14 +56,14 @@ ] }, "overscan_crop": "", - "width": 0, - "height": 0, "overscan_color": [ 0, 0, 0, 255 ], + "width": 0, + "height": 0, "bg_color": [ 0, 0, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 9f7a573df9..8ca203e3bc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -182,6 +182,16 @@ "key": "overscan_crop", "label": "Overscan crop" }, + { + "type": "label", + "label": "Overscan color is used when input aspect ratio is not same as output aspect ratio." + }, + { + "type": "color", + "label": "Overscan color", + "key": "overscan_color", + "use_alpha": false + }, { "type": "label", "label": "Width and Height must be both set to higher value than 0 else source resolution is used." @@ -202,16 +212,6 @@ "minimum": 0, "maximum": 100000 }, - { - "type": "label", - "label": "Overscan color is used only when output aspect pixel ratio is not same as input ratio." - }, - { - "type": "color", - "label": "Overscan color", - "key": "overscan_color", - "use_alpha": false - }, { "type": "label", "label": "Background color is used only when input have transparency and Alpha is higher than 0." From 2c6c381450a4e334a3ef935db5c0d693272d8814 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 12:37:00 +0200 Subject: [PATCH 067/359] updated output defs settings image --- .../global_extract_review_output_defs.png | Bin 19871 -> 26078 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/docs/project_settings/assets/global_extract_review_output_defs.png b/website/docs/project_settings/assets/global_extract_review_output_defs.png index ce3c00ca40387344fbcad18194a0fa73c82801f2..f4c1661b110d93aeab337e2556fb13ae5692ea16 100644 GIT binary patch literal 26078 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 literal 19871 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 From 9aec3761de8b920aba0635abf3c777bd3c2dd9ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 13:05:09 +0200 Subject: [PATCH 068/359] added basic infor about overscan color and background color --- .../docs/project_settings/settings_project_global.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 5d23dd75e6..5c46cd185a 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -112,6 +112,10 @@ Profile may generate multiple outputs from a single input. Each output must defi | "-10% -200px" | 1800px 800px | | "-10% -0px" | 1800px 1000px | +- **`Overscan color`** + - Color of empty area caused by different aspect ratio of input and output. + - By default is set to black color. + - **`Letter Box`** - **Enabled** - Enable letter boxes - **Ratio** - Ratio of letter boxes @@ -124,6 +128,14 @@ Profile may generate multiple outputs from a single input. Each output must defi ![global_extract_review_letter_box_settings](assets/global_extract_review_letter_box_settings.png) ![global_extract_review_letter_box](assets/global_extract_review_letter_box.png) +- **`Background color`** + - Background color can be used for inputs with possible transparency (e.g. png sequence). + - Input's without possible alpha channel are ignored all the time (e.g. mov). + - Background color slows down rendering process. + - set alpha to `0` to not use this option at all (in most of cases background stays black) + - other than `0` alpha will draw color as background + + ### IntegrateAssetNew Saves information for all published subsets into DB, published assets are available for other hosts, tools and tasks after. From d1f0003a927ebac997f3f898ee57c58836c0d320 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Jun 2021 14:04:46 +0200 Subject: [PATCH 069/359] Nuke: fixing version frame range prerender exception --- .../nuke/plugins/publish/precollect_writes.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 5eaac89e84..0b5fbc0479 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -1,5 +1,6 @@ import os import re +from pprint import pformat import nuke import pyblish.api import openpype.api as pype @@ -17,6 +18,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): def process(self, instance): _families_test = [instance.data["family"]] + instance.data["families"] + self.log.debug("_families_test: {}".format(_families_test)) node = None for x in instance: @@ -133,22 +135,29 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "outputDir": output_dir, "ext": ext, "label": label, - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStart": first_frame + handle_start, - "frameEnd": last_frame - handle_end, - "frameStartHandle": first_frame, - "frameEndHandle": last_frame, "outputType": output_type, "colorspace": colorspace, "deadlineChunkSize": deadlineChunkSize, "deadlinePriority": deadlinePriority }) - if "prerender" in _families_test: + if self.is_prerender(_families_test): instance.data.update({ - "family": "prerender", - "families": [] + "handleStart": 0, + "handleEnd": 0, + "frameStart": first_frame, + "frameEnd": last_frame, + "frameStartHandle": first_frame, + "frameEndHandle": last_frame, + }) + else: + instance.data.update({ + "handleStart": handle_start, + "handleEnd": handle_end, + "frameStart": first_frame + handle_start, + "frameEnd": last_frame - handle_end, + "frameStartHandle": first_frame, + "frameEndHandle": last_frame, }) # * Add audio to instance if exists. @@ -170,4 +179,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): "filename": api.get_representation_path(repre_doc) }] - self.log.debug("instance.data: {}".format(instance.data)) + self.log.debug("instance.data: {}".format(pformat(instance.data))) + + def is_prerender(self, families): + return next((f for f in families if "prerender" in f), None) From b2cc66ba2282934b04dc8b8fd2369bc08b39dcb1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 14:34:58 +0200 Subject: [PATCH 070/359] added smoothnes to alpha slider --- openpype/widgets/color_widgets/color_inputs.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index eda8c618f1..ada8befd65 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -80,7 +80,7 @@ class AlphaSlider(QtWidgets.QSlider): painter.fillRect(event.rect(), QtCore.Qt.transparent) - painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform) + painter.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) rect = self.style().subControlRect( QtWidgets.QStyle.CC_Slider, opt, @@ -135,19 +135,8 @@ class AlphaSlider(QtWidgets.QSlider): painter.save() - gradient = QtGui.QRadialGradient() - radius = handle_rect.height() / 2 - center_x = handle_rect.width() / 2 + handle_rect.x() - center_y = handle_rect.height() - gradient.setCenter(center_x, center_y) - gradient.setCenterRadius(radius) - gradient.setFocalPoint(center_x, center_y) - - gradient.setColorAt(0.9, QtGui.QColor(127, 127, 127)) - gradient.setColorAt(1, QtCore.Qt.transparent) - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(gradient) + painter.setBrush(QtGui.QColor(127, 127, 127)) painter.drawEllipse(handle_rect) painter.restore() From bf6504eac53ff536a6b677f89dd98b55a38924eb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 15 Jun 2021 14:35:09 +0200 Subject: [PATCH 071/359] removed unused slide_style --- .../widgets/color_widgets/color_inputs.py | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/openpype/widgets/color_widgets/color_inputs.py b/openpype/widgets/color_widgets/color_inputs.py index ada8befd65..6f5d4baa02 100644 --- a/openpype/widgets/color_widgets/color_inputs.py +++ b/openpype/widgets/color_widgets/color_inputs.py @@ -4,35 +4,6 @@ from Qt import QtWidgets, QtCore, QtGui from .color_view import draw_checkerboard_tile -slide_style = """ -QSlider::groove:horizontal { - background: qlineargradient( - x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #000, stop: 1 #fff - ); - height: 8px; - border-radius: 4px; -} - -QSlider::handle:horizontal { - background: qlineargradient( - x1:0, y1:0, x2:1, y2:1, stop:0 #ddd, stop:1 #bbb - ); - border: 1px solid #777; - width: 8px; - margin-top: -1px; - margin-bottom: -1px; - border-radius: 4px; -} - -QSlider::handle:horizontal:hover { - background: qlineargradient( - x1:0, y1:0, x2:1, y2:1, stop:0 #eee, stop:1 #ddd - ); - border: 1px solid #444;ff - border-radius: 4px; -}""" - - class AlphaSlider(QtWidgets.QSlider): def __init__(self, *args, **kwargs): super(AlphaSlider, self).__init__(*args, **kwargs) From 98a1b9973ee6d8118c395c9dc4bbc96594883931 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Jun 2021 15:12:21 +0200 Subject: [PATCH 072/359] Nuke: improving validator of rendered frames --- .../publish/validate_rendered_frames.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py index 8b71aff1ac..0c88014649 100644 --- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py +++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py @@ -61,7 +61,6 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): hosts = ["nuke", "nukestudio"] actions = [RepairCollectionActionToLocal, RepairCollectionActionToFarm] - def process(self, instance): for repre in instance.data["representations"]: @@ -78,10 +77,10 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): collection = collections[0] - frame_length = int( - instance.data["frameEndHandle"] - - instance.data["frameStartHandle"] + 1 - ) + fstartH = instance.data["frameStartHandle"] + fendH = instance.data["frameEndHandle"] + + frame_length = int(fendH - fstartH + 1) if frame_length != 1: if len(collections) != 1: @@ -95,7 +94,16 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): raise ValidationException(msg) collected_frames_len = int(len(collection.indexes)) + coll_start = min(collection.indexes) + coll_end = max(collection.indexes) + self.log.info("frame_length: {}".format(frame_length)) + self.log.info("collected_frames_len: {}".format( + collected_frames_len)) + self.log.info("fstartH-fendH: {}-{}".format(fstartH, fendH)) + self.log.info( + "coll_start-coll_end: {}-{}".format(coll_start, coll_end)) + self.log.info( "len(collection.indexes): {}".format(collected_frames_len) ) @@ -103,8 +111,11 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin): if ("slate" in instance.data["families"]) \ and (frame_length != collected_frames_len): collected_frames_len -= 1 + fstartH += 1 - assert (collected_frames_len == frame_length), ( + assert ((collected_frames_len >= frame_length) + and (coll_start <= fstartH) + and (coll_end >= fendH)), ( "{} missing frames. Use repair to render all frames" ).format(__name__) From c105eedbc7503f0ef8475ef26b0c68c53a023241 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 15:23:49 +0200 Subject: [PATCH 073/359] #680 - fixed logging, task selection --- .../plugins/publish/collect_ftrack_family.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index a66e6db29f..13ae40fd44 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -6,7 +6,9 @@ Provides: instance -> families ([]) """ import os + import pyblish.api +import avalon.api from openpype.lib.plugin_tools import filter_profiles @@ -31,10 +33,11 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): return anatomy_data = instance.context.data["anatomyData"] - task_name = instance.data("task", - instance.context.data["task"]) + task_name = instance.data.get("task", + instance.context.data.get("task", + avalon.api.Session["AVALON_TASK"])) host_name = anatomy_data.get("app", - os.environ["AVALON_APP"]) + avalon.api.Session["AVALON_APP"]) family = instance.data["family"] filtering_criteria = { @@ -47,7 +50,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if profile: families = instance.data.get("families") if profile["add_ftrack_family"]: - self.log.debug("Adding ftrack family") + self.log.debug("Adding ftrack family for '{}'". + format(instance.data.get("family"))) if families and "ftrack" not in families: instance.data["families"].append("ftrack") @@ -57,6 +61,6 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if families and "ftrack" in families: self.log.debug("Explicitly removing 'ftrack'") instance.data["families"].remove("ftrack") - - self.log.debug("Resulting families '{}' for '{}'".format( - instance.data["families"], instance.data["family"])) + else: + self.log.debug("Instance '{}' doesn't match any profile".format( + instance.data.get("family"))) From 2b202dd7599fcc1aafe337923040a345043f274f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 16:25:02 +0200 Subject: [PATCH 074/359] #680 - added remove "matchmove" to settings Removed unneeded plugin --- .../plugins/publish/collect_matchmove.py | 29 ------------------- .../defaults/project_settings/ftrack.json | 8 +++++ 2 files changed, 8 insertions(+), 29 deletions(-) delete mode 100644 openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py deleted file mode 100644 index 5d9e8ddfb4..0000000000 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py +++ /dev/null @@ -1,29 +0,0 @@ -""" -Requires: - Nothing - -Provides: - Instance -""" - -import pyblish.api -import logging - - -log = logging.getLogger("collector") - - -class CollectMatchmovePublish(pyblish.api.InstancePlugin): - """ - Collector with only one reason for its existence - remove 'ftrack' - family implicitly added by Standalone Publisher - """ - - label = "Collect Matchmove - SA Publish" - order = pyblish.api.CollectorOrder - families = ["matchmove"] - hosts = ["standalonepublisher"] - - def process(self, instance): - if "ftrack" in instance.data["families"]: - instance.data["families"].remove("ftrack") diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index a8e121d7c3..f83b6cef3a 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -211,6 +211,14 @@ ], "add_ftrack_family": true }, + { + "families": ["matchmove"], + "tasks": [], + "hosts": [ + "standalonepublisher" + ], + "add_ftrack_family": false + }, { "families": [ "model", From 4f0e30b46debfa44f2f4b8816248a0f9bca15f82 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 15 Jun 2021 16:26:09 +0200 Subject: [PATCH 075/359] #680 - removed default_families as now there are profiles in Setting doing that --- .../standalonepublisher/plugins/publish/collect_context.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py index d4855da140..6913e0836d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py @@ -34,7 +34,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): # presets batch_extensions = ["edl", "xml", "psd"] - default_families = [] def process(self, context): # get json paths from os and load them @@ -213,10 +212,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin): subset = in_data["subset"] # If instance data already contain families then use it instance_families = in_data.get("families") or [] - # Make sure default families are in instance - for default_family in self.default_families or []: - if default_family not in instance_families: - instance_families.append(default_family) instance = context.create_instance(subset) instance.data.update( From 692195bf9378e029e6f1acbf1cbeeec77b13ecd4 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 15 Jun 2021 14:35:12 +0000 Subject: [PATCH 076/359] [Automated] Bump version --- CHANGELOG.md | 14 +++++++++++--- openpype/version.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 537be94076..0b31b0ac45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # Changelog -## [3.1.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.1.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...HEAD) #### 🚀 Enhancements +- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) +- Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) +- OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) - Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) - \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683) - Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682) @@ -13,26 +16,31 @@ - Remove outline in stylesheet [\#1667](https://github.com/pypeclub/OpenPype/pull/1667) - TVPaint: Creator take layer name as default value for subset variant [\#1663](https://github.com/pypeclub/OpenPype/pull/1663) - TVPaint custom subset template [\#1662](https://github.com/pypeclub/OpenPype/pull/1662) +- Editorial: conform assets validator [\#1659](https://github.com/pypeclub/OpenPype/pull/1659) - Feature Slack integration [\#1657](https://github.com/pypeclub/OpenPype/pull/1657) - Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653) -- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650) - \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649) #### 🐛 Bug fixes +- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) +- Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) +- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) - Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) +- Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) - Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) - New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) +- Farm publishing: check if published items do exist [\#1573](https://github.com/pypeclub/OpenPype/pull/1573) **Merged pull requests:** +- update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) -- Add docstrings to Project manager tool [\#1556](https://github.com/pypeclub/OpenPype/pull/1556) # Changelog diff --git a/openpype/version.py b/openpype/version.py index bf261d41b2..448110d653 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.1.0-nightly.3" +__version__ = "3.1.0-nightly.4" From 91d75c89b0947a6e16df50726312d664a545d18c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Jun 2021 17:54:00 +0200 Subject: [PATCH 077/359] settings: fix imageio granularity --- .../projects_schema/schemas/schema_anatomy_imageio.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json index 3c589f9492..2b2eab8868 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_imageio.json @@ -3,7 +3,6 @@ "key": "imageio", "label": "Color Management and Output Formats", "is_file": true, - "is_group": true, "children": [ { "key": "hiero", @@ -15,6 +14,7 @@ "type": "dict", "label": "Workfile", "collapsible": false, + "is_group": true, "children": [ { "type": "form", @@ -89,6 +89,7 @@ "type": "dict", "label": "Colorspace on Inputs by regex detection", "collapsible": true, + "is_group": true, "children": [ { "type": "list", @@ -123,6 +124,7 @@ "type": "dict", "label": "Viewer", "collapsible": false, + "is_group": true, "children": [ { "type": "text", @@ -136,6 +138,7 @@ "type": "dict", "label": "Workfile", "collapsible": false, + "is_group": true, "children": [ { "type": "form", @@ -233,6 +236,7 @@ "type": "dict", "label": "Nodes", "collapsible": true, + "is_group": true, "children": [ { "key": "requiredNodes", @@ -335,6 +339,7 @@ "type": "dict", "label": "Colorspace on Inputs by regex detection", "collapsible": true, + "is_group": true, "children": [ { "type": "list", From 69dc652db35410e0d85f4ec351bd9e9cff213116 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 15 Jun 2021 17:54:43 +0200 Subject: [PATCH 078/359] nuke: default `create_directories` nuke write node https://github.com/pypeclub/client/issues/66 --- openpype/settings/defaults/project_anatomy/imageio.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index ff16c22663..fcebc876f5 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -78,6 +78,10 @@ { "name": "colorspace", "value": "linear" + }, + { + "name": "create_directories", + "value": "True" } ] }, @@ -114,6 +118,10 @@ { "name": "colorspace", "value": "linear" + }, + { + "name": "create_directories", + "value": "True" } ] } From 53112e04d67cd85cc87eb1265871daf67a6165e6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 15 Jun 2021 19:19:53 +0200 Subject: [PATCH 079/359] fix condition --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index fa1ce7f9a9..57e3f478f1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -72,7 +72,7 @@ class ExtractPlayblast(openpype.api.Extractor): # Isolate view is requested by having objects in the set besides a # camera. - if preset.pop("isolate_view", False) or instance.data.get("isolate"): + if preset.pop("isolate_view", False) and instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] # Show/Hide image planes on request. diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 5a91888781..aa8adc3986 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -75,7 +75,7 @@ class ExtractThumbnail(openpype.api.Extractor): # Isolate view is requested by having objects in the set besides a # camera. - if preset.pop("isolate_view", False) or instance.data.get("isolate"): + if preset.pop("isolate_view", False) and instance.data.get("isolate"): preset["isolate"] = instance.data["setMembers"] with maintained_time(): From e00072deae7719d3d1871ceb418642602fb5f86d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 15 Jun 2021 23:00:22 +0200 Subject: [PATCH 080/359] update release workflow --- .github/workflows/release.yml | 108 +++++++++++++++++++++------------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5d1822310..835657d2d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,9 @@ name: Stable Release on: - push: - tags: - - '*[0-9].*[0-9].*[0-9]*' + release: + types: + - prereleased jobs: create_release: @@ -23,27 +23,18 @@ jobs: - name: Install Python requirements run: pip install gitpython semver - - name: Set env - run: | - echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - git config user.email ${{ secrets.CI_EMAIL }} - git config user.name ${{ secrets.CI_USER }} - git fetch - git checkout -b main origin/main - git tag -d ${GITHUB_REF#refs/*/} - git remote set-url --push origin https://pypebot:${{ secrets.ADMIN_TOKEN }}@github.com/pypeclub/openpype - git push origin --delete ${GITHUB_REF#refs/*/} - echo PREVIOUS_VERSION=`git describe --tags --match="[0-9]*" --abbrev=0` >> $GITHUB_ENV - - name: 💉 Inject new version into files id: version - if: steps.version_type.outputs.type != 'skip' run: | - python ./tools/ci_tools.py --version ${{ env.RELEASE_VERSION }} + echo ::set-output name=current_version::${GITHUB_REF#refs/*/} + RESULT=$(python ./tools/ci_tools.py --finalize ${GITHUB_REF#refs/*/}) + LASTRELEASE=$(python ./tools/ci_tools.py --lastversion release) + + echo ::set-output name=last_release::$LASTRELEASE + echo ::set-output name=release_tag::$RESULT - name: "✏️ Generate full changelog" - if: steps.version_type.outputs.type != 'skip' + if: steps.version.outputs.release_tag != 'skip' id: generate-full-changelog uses: heinrichreimer/github-changelog-generator-action@v2.2 with: @@ -55,7 +46,6 @@ jobs: addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' issues: false issuesWoLabels: false - sinceTag: "3.0.0" maxIssues: 100 pullRequests: true prWoLabels: false @@ -64,39 +54,75 @@ jobs: compareLink: true stripGeneratorNotice: true verbose: true - futureRelease: ${{ env.RELEASE_VERSION }} + futureRelease: ${{ steps.version.outputs.release_tag }} excludeTagsRegex: "CI/.+" releaseBranch: "main" - - name: "🖨️ Print changelog to console" - run: echo ${{ steps.generate-last-changelog.outputs.changelog }} - - name: 💾 Commit and Tag id: git_commit - if: steps.version_type.outputs.type != 'skip' + if: steps.version.outputs.release_tag != 'skip' run: | + git config user.email ${{ secrets.CI_EMAIL }} + git config user.name ${{ secrets.CI_USER }} git add . git commit -m "[Automated] Release" - tag_name="${{ env.RELEASE_VERSION }}" - git push - git tag -fa $tag_name -m "stable release" - git remote set-url --push origin https://pypebot:${{ secrets.ADMIN_TOKEN }}@github.com/pypeclub/openpype - git push origin $tag_name + tag_name="${{ steps.version.outputs.release_tag }}" + git tag -a $tag_name -m "stable release" - - name: "🚀 Github Release" - uses: docker://antonyurchenko/git-release:latest - env: - GITHUB_TOKEN: ${{ secrets.ADMIN_TOKEN }} - DRAFT_RELEASE: "false" - PRE_RELEASE: "false" - CHANGELOG_FILE: "CHANGELOG.md" - ALLOW_EMPTY_CHANGELOG: "false" - ALLOW_TAG_PREFIX: "true" + - name: 🔏 Push to protected main branch + if: steps.version.outputs.release_tag != 'skip' + uses: CasperWA/push-protected@v2 + with: + token: ${{ secrets.ADMIN_TOKEN }} + branch: main + tags: true + unprotect_reviews: true + + - name: "✏️ Generate last changelog" + if: steps.version.outputs.release_tag != 'skip' + id: generate-last-changelog + uses: heinrichreimer/github-changelog-generator-action@v2.2 + with: + token: ${{ secrets.ADMIN_TOKEN }} + breakingLabel: '**💥 Breaking**' + enhancementLabel: '**🚀 Enhancements**' + bugsLabel: '**🐛 Bug fixes**' + deprecatedLabel: '**⚠️ Deprecations**' + addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' + issues: false + issuesWoLabels: false + maxIssues: 100 + pullRequests: true + prWoLabels: false + author: false + unreleased: true + compareLink: true + stripGeneratorNotice: true + verbose: true + futureRelease: ${{ steps.version.outputs.release_tag }} + excludeTagsRegex: "CI/.+" + releaseBranch: "main" + stripHeaders: true + sinceTag: ${{ steps.version.outputs.last_release }} - - name: 🔨 Merge main back to develop + - name: 🚀 Github Release + if: steps.version.outputs.release_tag != 'skip' + uses: ncipollo/release-action@v1 + with: + body: ${{ steps.generate-last-changelog.outputs.changelog }} + tag: ${{ steps.version.outputs.release_tag }} + token: ${{ secrets.ADMIN_TOKEN }} + + - name: ☠ Delete Pre-release + if: steps.version.outputs.release_tag != 'skip' + uses: cb80/delrel@latest + with: + tag: "${{ steps.version.outputs.current_version }}" + + - name: 🔁 Merge main back to develop + if: steps.version.outputs.release_tag != 'skip' uses: everlytic/branch-merge@1.1.0 - if: steps.version_type.outputs.type != 'skip' with: github_token: ${{ secrets.ADMIN_TOKEN }} source_ref: 'main' From b878df6c3bf7a74dc732ff9f9df93949beaaa5b1 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 15 Jun 2021 23:07:10 +0200 Subject: [PATCH 081/359] upgrade ci tools --- tools/ci_tools.py | 53 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tools/ci_tools.py b/tools/ci_tools.py index 85ef6bdcee..436551c243 100644 --- a/tools/ci_tools.py +++ b/tools/ci_tools.py @@ -92,6 +92,24 @@ def calculate_next_nightly(token="nightly"): next_tag = last_pre_v.bump_prerelease(token=token).__str__() return next_tag +def finalize_latest_nightly(): + last_prerelease, last_pre_tag = get_last_version("CI") + last_pre_v = VersionInfo.parse(last_prerelease) + last_pre_v_finalized = last_pre_v.finalize_version() + # print(last_pre_v_finalized) + + return last_pre_v_finalized.__str__() + +def finalize_prerelease(prerelease): + + if "/" in prerelease: + prerelease = prerelease.split("/")[-1] + + prerelease_v = VersionInfo.parse(prerelease) + prerelease_v_finalized = prerelease_v.finalize_version() + + return prerelease_v_finalized.__str__() + def main(): usage = "usage: %prog [options] arg" @@ -102,12 +120,22 @@ def main(): parser.add_option("-b", "--bump", dest="bump", action="store_true", help="Return if there is something to bump") - parser.add_option("-v", "--version", - dest="version", action="store", - help="work with explicit version") + parser.add_option("-r", "--release-latest", + dest="releaselatest", action="store_true", + help="finalize latest prerelease to a release") parser.add_option("-p", "--prerelease", dest="prerelease", action="store", help="define prerelease token") + parser.add_option("-f", "--finalize", + dest="finalize", action="store", + help="define prerelease token") + parser.add_option("-v", "--version", + dest="version", action="store", + help="work with explicit version") + parser.add_option("-l", "--lastversion", + dest="lastversion", action="store", + help="work with explicit version") + (options, args) = parser.parse_args() @@ -124,6 +152,25 @@ def main(): print(next_tag_v) bump_file_versions(next_tag_v) + if options.finalize: + new_release = finalize_prerelease(options.finalize) + print(new_release) + bump_file_versions(new_release) + + if options.lastversion: + last_release, last_release_tag = get_last_version(options.lastversion) + print(last_release_tag) + + if options.releaselatest: + new_release = finalize_latest_nightly() + last_release, last_release_tag = get_last_version("release") + + if VersionInfo.parse(new_release) > VersionInfo.parse(last_release): + print(new_release) + bump_file_versions(new_release) + else: + print("skip") + if options.prerelease: current_prerelease = VersionInfo.parse(options.prerelease) new_prerelease = current_prerelease.bump_prerelease().__str__() From 664c1823cbcb9949eccd41bbe594aee97b1d842f Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 15 Jun 2021 23:35:00 +0200 Subject: [PATCH 082/359] exclude history from last changelog --- .github/workflows/prerelease.yml | 8 ++++---- .github/workflows/release.yml | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 45604e431d..d0853e74d6 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -43,10 +43,10 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '#### 💥 Breaking' - enhancementLabel: '#### 🚀 Enhancements' - bugsLabel: '#### 🐛 Bug fixes' - deprecatedLabel: '#### ⚠️ Deprecations' + breakingLabel: '**💥 Breaking**' + enhancementLabel: '**🚀 Enhancements**' + bugsLabel: '**🐛 Bug fixes**' + deprecatedLabel: '**⚠️ Deprecations**' addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' issues: false issuesWoLabels: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 835657d2d2..e818929ffe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,13 +39,14 @@ jobs: uses: heinrichreimer/github-changelog-generator-action@v2.2 with: token: ${{ secrets.ADMIN_TOKEN }} - breakingLabel: '#### 💥 Breaking' - enhancementLabel: '#### 🚀 Enhancements' - bugsLabel: '#### 🐛 Bug fixes' - deprecatedLabel: '#### ⚠️ Deprecations' + breakingLabel: '**💥 Breaking**' + enhancementLabel: '**🚀 Enhancements**' + bugsLabel: '**🐛 Bug fixes**' + deprecatedLabel: '**⚠️ Deprecations**' addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' issues: false issuesWoLabels: false + sinceTag: "3.0.0" maxIssues: 100 pullRequests: true prWoLabels: false @@ -91,6 +92,7 @@ jobs: addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}' issues: false issuesWoLabels: false + sinceTag: ${{ steps.version.outputs.last_release }} maxIssues: 100 pullRequests: true prWoLabels: false @@ -103,7 +105,7 @@ jobs: excludeTagsRegex: "CI/.+" releaseBranch: "main" stripHeaders: true - sinceTag: ${{ steps.version.outputs.last_release }} + base: '' - name: 🚀 Github Release From 9a70c3d7aea8e3d58a582b81967031f49ab2d046 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 15 Jun 2021 21:41:17 +0000 Subject: [PATCH 083/359] [Automated] Release --- CHANGELOG.md | 10 ++++------ openpype/version.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b31b0ac45..043295eb8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,10 @@ # Changelog -## [3.1.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...3.1.0) -#### 🚀 Enhancements +**🚀 Enhancements** - Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) @@ -21,7 +21,7 @@ - Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653) - \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649) -#### 🐛 Bug fixes +**🐛 Bug fixes** - Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) @@ -33,9 +33,7 @@ - Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) - Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) -- Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) - New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) -- Farm publishing: check if published items do exist [\#1573](https://github.com/pypeclub/OpenPype/pull/1573) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 448110d653..4312333660 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.1.0-nightly.4" +__version__ = "3.1.0" From cfc3e49f3209f04714be46e24b728dc2879208f9 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 16 Jun 2021 00:00:10 +0200 Subject: [PATCH 084/359] cut out history from last changelog --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e818929ffe..37e1cb4b15 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -105,7 +105,7 @@ jobs: excludeTagsRegex: "CI/.+" releaseBranch: "main" stripHeaders: true - base: '' + base: 'none' - name: 🚀 Github Release From bca04289ef1771f96679ab3da94fb957e9b0fdff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 12:16:33 +0200 Subject: [PATCH 085/359] set default subset template for review family in tvpaint --- .../settings/defaults/project_settings/global.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index b7fa5e32e8..c3c43c3b3b 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -232,6 +232,16 @@ ], "tasks": [], "template": "{family}{Task}_{Render_layer}_{Render_pass}" + }, + { + "families": [ + "review" + ], + "hosts": [ + "tvpaint" + ], + "tasks": [], + "template": "{family}{Task}" } ] }, From 29271dc0c777234a7ad3952fef50ed42a6d3ea2c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Jun 2021 12:19:11 +0200 Subject: [PATCH 086/359] #680 - added additional filtering on 'families' instead of main 'family' --- .../plugins/publish/collect_ftrack_family.py | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 13ae40fd44..8dbcb0ab2c 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -5,8 +5,6 @@ Requires: Provides: instance -> families ([]) """ -import os - import pyblish.api import avalon.api @@ -20,10 +18,14 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): Uses selection by combination of hosts/families/tasks names via profiles resolution. - Triggered everywhere, checks instance against configured + Triggered everywhere, checks instance against configured. + + Checks advanced filtering which works on 'families' not on main + 'family', as some variants dynamically resolves addition of ftrack + based on 'families' (editorial drives it by presence of 'review') """ label = "Collect Ftrack Family" - order = pyblish.api.CollectorOrder + 0.4999 + order = pyblish.api.CollectorOrder + 0.4998 profiles = None @@ -34,8 +36,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): anatomy_data = instance.context.data["anatomyData"] task_name = instance.data.get("task", - instance.context.data.get("task", - avalon.api.Session["AVALON_TASK"])) + avalon.api.Session["AVALON_TASK"]) host_name = anatomy_data.get("app", avalon.api.Session["AVALON_APP"]) family = instance.data["family"] @@ -49,7 +50,17 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): if profile: families = instance.data.get("families") - if profile["add_ftrack_family"]: + add_ftrack_family = profile["add_ftrack_family"] + + additional_filters = profile.get("additional_filters") + if additional_filters: + add_ftrack_family = self._get_add_ftrack_f_from_addit_filters( + additional_filters, + families, + add_ftrack_family + ) + + if add_ftrack_family: self.log.debug("Adding ftrack family for '{}'". format(instance.data.get("family"))) @@ -57,10 +68,41 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): instance.data["families"].append("ftrack") else: instance.data["families"] = ["ftrack"] - else: - if families and "ftrack" in families: - self.log.debug("Explicitly removing 'ftrack'") - instance.data["families"].remove("ftrack") else: self.log.debug("Instance '{}' doesn't match any profile".format( instance.data.get("family"))) + + def _get_add_ftrack_f_from_addit_filters(self, + additional_filters, + families, + add_ftrack_family): + """ + Compares additional filters - working on instance's families. + + Triggered for more detailed filtering when main family matches, + but content of 'families' actually matter. + (For example 'review' in 'families' should result in adding to + Ftrack) + + Args: + additional_filters (dict) - from Setting + families (list) - subfamilies + add_ftrack_family (bool) - add ftrack to families if True + """ + override_filter = None + override_filter_value = -1 + for additional_filter in additional_filters: + filter_families = set(additional_filter["families"]) + valid = filter_families <= families # issubset + if not valid: + continue + + value = len(filter_families) + if value > override_filter_value: + override_filter = additional_filter + override_filter_value = value + + if override_filter: + add_ftrack_family = override_filter["add_ftrack_family"] + + return add_ftrack_family From 18b8666b8166a89f570bbfd5d3c43a07e7327378 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 12:20:57 +0200 Subject: [PATCH 087/359] added workfile to families --- openpype/settings/defaults/project_settings/global.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index c3c43c3b3b..037fa63a29 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -235,7 +235,8 @@ }, { "families": [ - "review" + "review", + "workfile" ], "hosts": [ "tvpaint" From b5900787079dd0f62c9a20911458a5d48ee1be6f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 16 Jun 2021 12:37:36 +0200 Subject: [PATCH 088/359] add PySide2 installation --- openpype/hosts/unreal/api/lib.py | 54 +++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index e069ac5256..56f92088b3 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -182,6 +182,36 @@ def create_unreal_project(project_name: str, """ env = env or os.environ preset = get_project_settings(project_name)["unreal"]["project_setup"] + ue_id = ".".join(ue_version.split(".")[:2]) + # get unreal engine identifier + # ------------------------------------------------------------------------- + # FIXME (antirotor): As of 4.26 this is problem with UE4 built from + # sources. In that case Engine ID is calculated per machine/user and not + # from Engine files as this code then reads. This then prevents UE4 + # to directly open project as it will complain about project being + # created in different UE4 version. When user convert such project + # to his UE4 version, Engine ID is replaced in uproject file. If some + # other user tries to open it, it will present him with similar error. + if platform.system().lower() == "windows": + ue4_modules = os.path.join(engine_path, "Engine", "Binaries", + "Win64", "UE4Editor.modules") + + if platform.system().lower() == "linux": + ue4_modules = os.path.join(engine_path, "Engine", "Binaries", + "Linux", "UE4Editor.modules") + + if platform.system().lower() == "darwin": + ue4_modules = os.path.join(engine_path, "Engine", "Binaries", + "Mac", "UE4Editor.modules") + + if os.path.exists(ue4_modules): + print("--- Loading Engine ID from modules file ...") + with open(ue4_modules, "r") as mp: + loaded_modules = json.load(mp) + + if loaded_modules.get("BuildId"): + ue_id = "{" + loaded_modules.get("BuildId") + "}" + plugins_path = None uep_path = None @@ -202,7 +232,7 @@ def create_unreal_project(project_name: str, # data for project file data = { "FileVersion": 3, - "EngineAssociation": ue_version, + "EngineAssociation": ue_id, "Category": "", "Description": "", "Plugins": [ @@ -305,6 +335,28 @@ def create_unreal_project(project_name: str, "pip", "install", "pyside"]) else: raise NotImplementedError("Unsupported platform") + else: + # install PySide2 inside newer engines + if platform.system().lower() == "windows": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python3", "Win64", + "python3.exe") + + if platform.system().lower() == "linux": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python3", "Linux", + "bin", "python3") + + if platform.system().lower() == "darwin": + python_path = os.path.join(engine_path, "Engine", "Binaries", + "ThirdParty", "Python3", "Mac", + "bin", "python3") + + if python_path: + subprocess.run([python_path, "-m", + "pip", "install", "pyside2"]) + else: + raise NotImplementedError("Unsupported platform") if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) From 00998f310d650dfa54bcfcfdd07e2397fdda8778 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 14:33:58 +0200 Subject: [PATCH 089/359] initial commit of subset name template docstrings --- .../settings_project_global.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 5c46cd185a..4bda13f91b 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -172,6 +172,38 @@ Applicable context filters: ## Tools Settings for OpenPype tools. +## Creator +Settings related to [Creator tool](artist_tools.md#details). + +### Subset name profiles +![global_tools_creator_subset_template](assets/global_tools_creator_subset_template.png) +Subset name helps to identify published content. More specific name helps with organization and avoid mixing of content. Subset name is defined using one of templates defined in Subset name profiles settings. The template is filled with information from context in which creation was triggered. + +Templates in settings are filtered by creator's family, host and task name. Template without filters is used as default template. It is recommend to set default template. If default template is not available `"{family}{Task}"` is used. + +**Formatting keys** + +All templates can contain text and formatting keys **family**, **task** and **variant** e.g. `"MyStudio_{family}_{task}"` (example - not recommended in production). + +|Key|Description| +|---|---| +|family|Creators family| +|task|Task under which is creation triggered| +|variant|User input in creator tool| + +**Formatting keys have 3 variants with different letter capitalization.** + +|Task|Key variant|Description|Result| +|---|---|---|---| +|`bgAnim`|`{task}`|Keep original value as is.|`bgAnim`| +|`bgAnim`|`{Task}`|Capitalize first letter of value.|`BgAnim`| +|`bgAnim`|`{TASK}`|Each letter which be capitalized.|`BGANIM`| + +Template may look like `"{family}{Task}{Variant}"`. + +Some creators may have other keys as their context may require more information or more specific values. Make sure you've read documentation of host you're using. + + ## Workfiles All settings related to Workfile tool. From d0cfbb03bdf22974d1665e4ef29f7d7b56d211f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 14:34:08 +0200 Subject: [PATCH 090/359] added screenshot --- .../global_tools_creator_subset_template.png | Bin 0 -> 17550 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/project_settings/assets/global_tools_creator_subset_template.png diff --git a/website/docs/project_settings/assets/global_tools_creator_subset_template.png b/website/docs/project_settings/assets/global_tools_creator_subset_template.png new file mode 100644 index 0000000000000000000000000000000000000000..c4e863c4e0f28982c91e0d4768d113af200e4440 GIT binary patch literal 17550 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 Date: Wed, 16 Jun 2021 15:22:47 +0200 Subject: [PATCH 091/359] added admin tvpaint host documentation with families --- website/docs/admin_hosts_tvpaint.md | 41 +++++++++++++++++++++++++++++ website/sidebars.js | 3 ++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 website/docs/admin_hosts_tvpaint.md diff --git a/website/docs/admin_hosts_tvpaint.md b/website/docs/admin_hosts_tvpaint.md new file mode 100644 index 0000000000..4db8f89166 --- /dev/null +++ b/website/docs/admin_hosts_tvpaint.md @@ -0,0 +1,41 @@ +--- +id: admin_hosts_tvpaint +title: TVPaint +sidebar_label: TVPaint +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Families +Families that can be published form TVPaint using OpenPype integration. + +### renderLayer +Render layer is represented by TVPaint group and all layers under the group. Output of `renderLayer` family are all visible layers rendered together into png sequence. + +Render layer has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. + +- Key **render_layer** is alias for variant (user's input). +- For key **render_pass** is used predefined value `"Beauty"` (ATM value can't be changed). + +### renderPass +Render pass is represented by one or more TVPaint layers. Is dependent on created `renderLayer`. All layers must be in same group of `renderLayer`. + +Render pass has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. +- Key **render_layer** is filled with value of **render_pass** from `renderLayer` group. +- Key **render_pass** is alias for variant (user's input). + +:::note Subset name template +It is recommended to use same subset name template for both **renderLayer** and **renderPass** families. +- Example template: `"{family}{Task}_{Render_layer}_{Render_pass}"` +::: + +### review +Review of whole scene. Exports all visible layers into sequence which is then processed in ExtractReview plugin. It is possible to deactivate publishing of review after collection. + +### workfile +Publish workfile and create it's backup outside of workfiles directory. It is possible to deactivate publishing of workfile after collection. + +:::note Dynamic families +Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. Recommented subset name template is `"{family}{Task}"`. +::: diff --git a/website/sidebars.js b/website/sidebars.js index 59071ec34f..d38973e40f 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -90,7 +90,8 @@ module.exports = { "admin_hosts_maya", "admin_hosts_resolve", "admin_hosts_harmony", - "admin_hosts_aftereffects" + "admin_hosts_aftereffects", + "admin_hosts_tvpaint" ], }, { From 56b858defa287de328d6ba54508845ed30daa7f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 15:34:36 +0200 Subject: [PATCH 092/359] removed most of not important stuff from admin section --- website/docs/admin_hosts_tvpaint.md | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/website/docs/admin_hosts_tvpaint.md b/website/docs/admin_hosts_tvpaint.md index 4db8f89166..6c9c5ff881 100644 --- a/website/docs/admin_hosts_tvpaint.md +++ b/website/docs/admin_hosts_tvpaint.md @@ -7,35 +7,24 @@ sidebar_label: TVPaint import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -## Families -Families that can be published form TVPaint using OpenPype integration. - -### renderLayer -Render layer is represented by TVPaint group and all layers under the group. Output of `renderLayer` family are all visible layers rendered together into png sequence. +## Subset name templates +Definition of possibile subset name templates in TVPaint integration. +### [Render Layer](artist_hosts_tvpaint#render-layer) Render layer has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. - Key **render_layer** is alias for variant (user's input). - For key **render_pass** is used predefined value `"Beauty"` (ATM value can't be changed). -### renderPass -Render pass is represented by one or more TVPaint layers. Is dependent on created `renderLayer`. All layers must be in same group of `renderLayer`. - +### [Render pass](artist_hosts_tvpaint#render-pass) Render pass has additional keys for subset name template. It is possible to use **render_layer** and **render_pass**. - Key **render_layer** is filled with value of **render_pass** from `renderLayer` group. - Key **render_pass** is alias for variant (user's input). -:::note Subset name template +:::important Render Layer/Pass templates It is recommended to use same subset name template for both **renderLayer** and **renderPass** families. - Example template: `"{family}{Task}_{Render_layer}_{Render_pass}"` ::: -### review -Review of whole scene. Exports all visible layers into sequence which is then processed in ExtractReview plugin. It is possible to deactivate publishing of review after collection. - -### workfile -Publish workfile and create it's backup outside of workfiles directory. It is possible to deactivate publishing of workfile after collection. - -:::note Dynamic families -Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. Recommented subset name template is `"{family}{Task}"`. -::: +### [Review](artist_hosts_tvpaint#review) and Workfile +Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. From 774595fba8fada980453e0aed3e650ccb99ef043 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 15:43:50 +0200 Subject: [PATCH 093/359] updated review and workfile families in tvpaint docs --- website/docs/artist_hosts_tvpaint.md | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/website/docs/artist_hosts_tvpaint.md b/website/docs/artist_hosts_tvpaint.md index 19cb615158..2e831e64d8 100644 --- a/website/docs/artist_hosts_tvpaint.md +++ b/website/docs/artist_hosts_tvpaint.md @@ -45,7 +45,7 @@ In TVPaint you can find the Tools in OpenPype menu extension. The OpenPype Tools ## Create -In TVPaint you can create and publish **[Reviews](#review)**, **[Render Passes](#render-pass)**, and **[Render Layers](#render-layer)**. +In TVPaint you can create and publish **[Reviews](#review)**, **[Workfile](#workfile)**, **[Render Passes](#render-pass)** and **[Render Layers](#render-layer)**. You have the possibility to organize your layers by using `Color group`. @@ -67,26 +67,13 @@ OpenPype specifically never tries to guess what you want to publish from the sce When you want to publish `review` or `render layer` or `render pass`, open the `Creator` through the Tools menu `Create` button. -### Review +### Review +`Review` renders the whole file as is and sends the resulting QuickTime to Ftrack. +- Is automatically created during publishing. -
-
- -`Review` renders the whole file as is and sends the resulting QuickTime to Ftrack. - -To create reviewable quicktime of your animation: - -- select `Review` in the `Creator` -- press `Create` -- When you run [publish](#publish), file will be rendered and converted to quicktime.` - -
-
- -![createreview](assets/tvp_create_review.png) - -
-
+### Workfile +`Workfile` stores the source workfile as is during publishing (e.g. for backup). +- Is automatically created during publishing. ### Render Layer From 776e784df9c111cec4914e74e315fa62d615d790 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 15:44:07 +0200 Subject: [PATCH 094/359] added link to workfile family --- website/docs/admin_hosts_tvpaint.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_tvpaint.md b/website/docs/admin_hosts_tvpaint.md index 6c9c5ff881..a99cd19010 100644 --- a/website/docs/admin_hosts_tvpaint.md +++ b/website/docs/admin_hosts_tvpaint.md @@ -26,5 +26,5 @@ It is recommended to use same subset name template for both **renderLayer** and - Example template: `"{family}{Task}_{Render_layer}_{Render_pass}"` ::: -### [Review](artist_hosts_tvpaint#review) and Workfile +### [Review](artist_hosts_tvpaint#review) and [Workfile](artist_hosts_tvpaint#workfile) Families **review** and **workfile** are not manually created but are automatically generated during publishing. That's why it is recommended to not use **variant** key in their subset name template. From a9d51ce003b99b6d8c51e3ef7394d257c71ded40 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 15:44:15 +0200 Subject: [PATCH 095/359] removed unused image --- website/docs/assets/tvp_create_review.png | Bin 30635 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 website/docs/assets/tvp_create_review.png diff --git a/website/docs/assets/tvp_create_review.png b/website/docs/assets/tvp_create_review.png deleted file mode 100644 index d6e9f6342850dda4afbc534b4678dacd8366b264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30635 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 Date: Wed, 16 Jun 2021 15:51:20 +0200 Subject: [PATCH 096/359] rephrasing subset templates --- website/docs/project_settings/settings_project_global.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 4bda13f91b..e6336c36e2 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -177,9 +177,10 @@ Settings related to [Creator tool](artist_tools.md#details). ### Subset name profiles ![global_tools_creator_subset_template](assets/global_tools_creator_subset_template.png) -Subset name helps to identify published content. More specific name helps with organization and avoid mixing of content. Subset name is defined using one of templates defined in Subset name profiles settings. The template is filled with information from context in which creation was triggered. -Templates in settings are filtered by creator's family, host and task name. Template without filters is used as default template. It is recommend to set default template. If default template is not available `"{family}{Task}"` is used. +Subset name helps to identify published content. More specific name helps with organization and avoid mixing of published content. Subset name is defined using one of templates defined in **Subset name profiles settings**. The template is filled with context information at the time of creation. + +Usage of template is defined by profile filtering using creator's family, host and task name. Profile without filters is used as default template and it is recommend to set default template. If default template is not available `"{family}{Task}"` is used. **Formatting keys** From eb8de6b3fa2b6789aace940017d23ff6c3bcb0dc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 16 Jun 2021 16:45:22 +0200 Subject: [PATCH 097/359] delete group apps without inputs --- openpype/tools/settings/local_settings/apps_widget.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/local_settings/apps_widget.py b/openpype/tools/settings/local_settings/apps_widget.py index f228ecc826..e6a4132955 100644 --- a/openpype/tools/settings/local_settings/apps_widget.py +++ b/openpype/tools/settings/local_settings/apps_widget.py @@ -196,8 +196,12 @@ class LocalApplicationsWidgets(QtWidgets.QWidget): # Create App group specific widget and store it by the key group_widget = AppGroupWidget(entity, self) - self.widgets_by_group_name[key] = group_widget - self.content_layout.addWidget(group_widget) + if group_widget.widgets_by_variant_name: + self.widgets_by_group_name[key] = group_widget + self.content_layout.addWidget(group_widget) + else: + group_widget.setVisible(False) + group_widget.deleteLater() def update_local_settings(self, value): if not value: From 855d5a9ae410fcd67405e90c51fb16b1d2120e80 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 13:46:59 +0200 Subject: [PATCH 098/359] use layer name instead of group name for default render layer variant --- .../plugins/create/create_render_layer.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py index eeb7d32d50..af6c0f0eee 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py @@ -58,18 +58,14 @@ class CreateRenderlayer(plugin.Creator): # Get currently selected layers layers_data = lib.layers_data() - group_ids = set() - for layer in layers_data: - if layer["selected"]: - group_ids.add(layer["group_id"]) - + selected_layers = [ + layer + for layer in layers_data + if layer["selected"] + ] # Return layer name if only one is selected - if len(group_ids) == 1: - group_id = list(group_ids)[0] - groups_data = lib.groups_data() - for group in groups_data: - if group["group_id"] == group_id: - return group["name"] + if len(selected_layers) == 1: + return selected_layers[0]["name"] # Use defaults if cls.defaults: From cdbfe669c8df6b777e38cff15c71549d3d34c175 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 17 Jun 2021 13:47:23 +0200 Subject: [PATCH 099/359] raise exceptions when ue not found --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 7c4b6c3088..c292730fb1 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -4,7 +4,8 @@ import os from openpype.lib import ( PreLaunchHook, - ApplicationLaunchFailed + ApplicationLaunchFailed, + ApplicationNotFound ) from openpype.hosts.unreal.api import lib as unreal_lib @@ -62,6 +63,8 @@ class UnrealPrelaunchHook(PreLaunchHook): f"{self.signature} detected UE4 versions: " f"[ {detected_str} ]" )) + if not detected: + raise ApplicationNotFound("No Unreal Engines are found.") engine_version = ".".join(engine_version.split(".")[:2]) if engine_version not in detected.keys(): From 4ecc8669717faf0e4113723ea04f645dabe32276 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:00:13 +0200 Subject: [PATCH 100/359] ProjectModel has resetless logic of refresh --- openpype/tools/launcher/models.py | 52 +++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 25b6dcdbf0..846a07e081 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -325,19 +325,59 @@ class ProjectModel(QtGui.QStandardItemModel): self.hide_invisible = False self.project_icon = qtawesome.icon("fa.map", color="white") + self._project_names = set() def refresh(self): - self.clear() - self.beginResetModel() - + project_names = set() for project_doc in self.get_projects(): - item = QtGui.QStandardItem(self.project_icon, project_doc["name"]) - self.appendRow(item) + project_names.add(project_doc["name"]) - self.endResetModel() + origin_project_names = set(self._project_names) + self._project_names = project_names + + project_names_to_remove = origin_project_names - project_names + if project_names_to_remove: + row_counts = {} + continuous = None + for row in range(self.rowCount()): + index = self.index(row, 0) + index_name = index.data(QtCore.Qt.DisplayRole) + if index_name in project_names_to_remove: + if continuous is None: + continuous = row + row_counts[continuous] = 0 + row_counts[continuous] += 1 + else: + continuous = None + + for row in reversed(sorted(row_counts.keys())): + count = row_counts[row] + self.removeRows(row, count) + + continuous = None + row_counts = {} + for idx, project_name in enumerate(sorted(project_names)): + if project_name in origin_project_names: + continuous = None + continue + + if continuous is None: + continuous = idx + row_counts[continuous] = [] + + row_counts[continuous].append(project_name) + + for row in reversed(sorted(row_counts.keys())): + items = [] + for project_name in row_counts[row]: + item = QtGui.QStandardItem(self.project_icon, project_name) + items.append(item) + + self.invisibleRootItem().insertRows(row, items) def get_projects(self): project_docs = [] + for project_doc in sorted( self.dbcon.projects(), key=lambda x: x["name"] ): From 912c6b3b575e4e66e7de35168890917cc2b810a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:08:45 +0200 Subject: [PATCH 101/359] added refresh timer which will update projects each 10 seconds --- openpype/tools/launcher/widgets.py | 22 ++++++++++++++++++++++ openpype/tools/launcher/window.py | 22 ++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 22b08d7d15..59fa2b729e 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -22,6 +22,9 @@ from .constants import ( class ProjectBar(QtWidgets.QWidget): project_changed = QtCore.Signal(int) + # Project list will be refreshed each 10000 msecs + refresh_interval = 10000 + def __init__(self, dbcon, parent=None): super(ProjectBar, self).__init__(parent) @@ -47,14 +50,19 @@ class ProjectBar(QtWidgets.QWidget): QtWidgets.QSizePolicy.Maximum ) + refresh_timer = QtCore.QTimer() + refresh_timer.setInterval(self.refresh_interval) + self.model = model self.project_delegate = project_delegate self.project_combobox = project_combobox + self.refresh_timer = refresh_timer # Initialize self.refresh() # Signals + refresh_timer.timeout.connect(self._on_refresh_timeout) self.project_combobox.currentIndexChanged.connect(self.project_changed) # Set current project by default if it's set. @@ -62,6 +70,20 @@ class ProjectBar(QtWidgets.QWidget): if project_name: self.set_project(project_name) + def showEvent(self, event): + if not self.refresh_timer.isActive(): + self.refresh_timer.start() + super(ProjectBar, self).showEvent(event) + + def _on_refresh_timeout(self): + if not self.isVisible(): + # Stop timer if widget is not visible + self.refresh_timer.stop() + + elif self.isActiveWindow(): + # Refresh projects if window is active + self.model.refresh() + def get_current_project(self): return self.project_combobox.currentText() diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index af749814b7..25aa273ca0 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -91,6 +91,8 @@ class ProjectsPanel(QtWidgets.QWidget): """Projects Page""" project_clicked = QtCore.Signal(str) + # Refresh projects each 10000 msecs + refresh_interval = 10000 def __init__(self, dbcon, parent=None): super(ProjectsPanel, self).__init__(parent=parent) @@ -111,16 +113,36 @@ class ProjectsPanel(QtWidgets.QWidget): layout.addWidget(view) + refresh_timer = QtCore.QTimer() + refresh_timer.setInterval(self.refresh_interval) + + refresh_timer.timeout.connect(self._on_refresh_timeout) view.clicked.connect(self.on_clicked) self.model = model self.view = view + self.refresh_timer = refresh_timer def on_clicked(self, index): if index.isValid(): project_name = index.data(QtCore.Qt.DisplayRole) self.project_clicked.emit(project_name) + def showEvent(self, event): + self.model.refresh() + if not self.refresh_timer.isActive(): + self.refresh_timer.start() + super(ProjectsPanel, self).showEvent(event) + + def _on_refresh_timeout(self): + if not self.isVisible(): + # Stop timer if widget is not visible + self.refresh_timer.stop() + + elif self.isActiveWindow(): + # Refresh projects if window is active + self.model.refresh() + class AssetsPanel(QtWidgets.QWidget): """Assets page""" From d6e345addc67006c2e1d48e30c9a4c0c0b95e378 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:09:09 +0200 Subject: [PATCH 102/359] removed unnecessary refresh calls --- openpype/tools/launcher/window.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 25aa273ca0..5e47cfd154 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -108,7 +108,6 @@ class ProjectsPanel(QtWidgets.QWidget): flick.activateOn(view) model = ProjectModel(self.dbcon) model.hide_invisible = True - model.refresh() view.setModel(model) layout.addWidget(view) @@ -434,7 +433,6 @@ class LauncherWindow(QtWidgets.QDialog): def on_back_clicked(self): self.dbcon.Session["AVALON_PROJECT"] = None self.set_page(0) - self.project_panel.model.refresh() # Refresh projects self.discover_actions() def on_action_clicked(self, action): From 8d9ccaf45bb6a0c5d96455359eb5d43fb317aee8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:15:27 +0200 Subject: [PATCH 103/359] simplified refreshing of project bar --- openpype/tools/launcher/widgets.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 59fa2b729e..11301aba64 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -91,27 +91,14 @@ class ProjectBar(QtWidgets.QWidget): index = self.project_combobox.findText(project_name) if index < 0: # Try refresh combobox model - self.project_combobox.blockSignals(True) - self.model.refresh() - self.project_combobox.blockSignals(False) - + self.refresh() index = self.project_combobox.findText(project_name) if index >= 0: self.project_combobox.setCurrentIndex(index) def refresh(self): - prev_project_name = self.get_current_project() - - # Refresh without signals - self.project_combobox.blockSignals(True) - self.model.refresh() - self.set_project(prev_project_name) - - self.project_combobox.blockSignals(False) - - self.project_changed.emit(self.project_combobox.currentIndex()) class ActionBar(QtWidgets.QWidget): From 193296ffaa63732eb43938b7b81e247218d76156 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:15:44 +0200 Subject: [PATCH 104/359] dont refresh on init --- openpype/tools/launcher/widgets.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 11301aba64..0e8caeb278 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -58,9 +58,6 @@ class ProjectBar(QtWidgets.QWidget): self.project_combobox = project_combobox self.refresh_timer = refresh_timer - # Initialize - self.refresh() - # Signals refresh_timer.timeout.connect(self._on_refresh_timeout) self.project_combobox.currentIndexChanged.connect(self.project_changed) From 5872a1364caeaa0739b5b9cc421644fd6e66dbf4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 17 Jun 2021 15:24:54 +0200 Subject: [PATCH 105/359] added refresh timer for actions --- openpype/tools/launcher/window.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 5e47cfd154..a6d34bbe9d 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -297,6 +297,8 @@ class AssetsPanel(QtWidgets.QWidget): class LauncherWindow(QtWidgets.QDialog): """Launcher interface""" + # Refresh actions each 10000msecs + actions_refresh_timeout = 10000 def __init__(self, parent=None): super(LauncherWindow, self).__init__(parent) @@ -365,6 +367,10 @@ class LauncherWindow(QtWidgets.QDialog): layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) + actions_refresh_timer = QtCore.QTimer() + actions_refresh_timer.setInterval(self.actions_refresh_timeout) + + self.actions_refresh_timer = actions_refresh_timer self.message_label = message_label self.project_panel = project_panel self.asset_panel = asset_panel @@ -374,6 +380,7 @@ class LauncherWindow(QtWidgets.QDialog): self._page = 0 # signals + actions_refresh_timer.timeout.connect(self._on_action_timer) actions_bar.action_clicked.connect(self.on_action_clicked) action_history.trigger_history.connect(self.on_history_action) project_panel.project_clicked.connect(self.on_project_clicked) @@ -388,9 +395,11 @@ class LauncherWindow(QtWidgets.QDialog): self.resize(520, 740) def showEvent(self, event): - super().showEvent(event) - # TODO implement refresh/reset which will trigger updating - self.discover_actions() + if not self.actions_refresh_timer.isActive(): + self.actions_refresh_timer.start() + self.discover_actions() + + super(LauncherWindow, self).showEvent(event) def set_page(self, page): current = self.page_slider.currentIndex() @@ -423,6 +432,15 @@ class LauncherWindow(QtWidgets.QDialog): def filter_actions(self): self.actions_bar.filter_actions() + def _on_action_timer(self): + if not self.isVisible(): + # Stop timer if widget is not visible + self.actions_refresh_timer.stop() + + elif self.isActiveWindow(): + # Refresh projects if window is active + self.discover_actions() + def on_project_clicked(self, project_name): self.dbcon.Session["AVALON_PROJECT"] = project_name # Refresh projects From 68e9102f7263e70c50c7f7a959ae48ba5c16280e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Jun 2021 17:04:45 +0200 Subject: [PATCH 106/359] #636 - PS created loader to load reference frame from 'render' sequence Loader cannot do containerization for now as updates would be too difficult. (Customer doesn't mind.) --- .../plugins/load/load_image_from_sequence.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py new file mode 100644 index 0000000000..1409065d43 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -0,0 +1,90 @@ +import os +from avalon import photoshop +from avalon.vendor import qargparse + +from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader + +stub = photoshop.stub() + + +class ImageFromSequenceLoader(ImageLoader): + """ Load specifing image from sequence + + Used only as quick load of reference file from a sequence. + + Plain ImageLoader picks first frame from sequence. + + Loads only existing files - currently not possible to limit loaders + to single select - multiselect. If user selects multiple repres, list + for all of them is provided, but selection is only single file. + This loader will be triggered multiple times, but selected name will + match only to proper path. + + Loader doesnt do containerization as there is currently no data model + of 'frame of rendered files' (only rendered sequence), update would be + difficult. + """ + + families = ["render"] + representations = ["*"] + options = [] + + def load(self, context, name=None, namespace=None, data=None): + if data.get("frame"): + self.fname = os.path.join(os.path.dirname(self.fname), + data["frame"]) + if not os.path.exists(self.fname): + return + + layer_name = self._get_unique_layer_name(context["asset"]["name"], + name) + with photoshop.maintained_selection(): + layer = stub.import_smart_object(self.fname, layer_name) + + self[:] = [layer] + namespace = namespace or layer_name + + return namespace + + @classmethod + def get_options(cls, repre_contexts): + """ + Returns list of files for selected 'repre_contexts'. + + It returns only files with same extension as in context as it is + expected that context points to sequence of frames. + + Returns: + (list) of qargparse.Choice + """ + files = [] + for context in repre_contexts: + fname = ImageLoader.filepath_from_context(context) + _, file_extension = os.path.splitext(fname) + + for file_name in os.listdir(os.path.dirname(fname)): + if not file_name.endswith(file_extension): + continue + files.append(file_name) + + # return selection only if there is something + if not files or len(files) <= 1: + return [] + + return [ + qargparse.Choice( + "frame", + label="Select specific file", + items=files, + default=0, + help="Which frame should be loaded?" + ) + ] + + def update(self, container, representation): + """No update possible, not containerized.""" + pass + + def remove(self, container): + """No update possible, not containerized.""" + pass From 852a2f35aa9352e3d9b3efe8b95d96c5697b7aa1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Jun 2021 18:30:15 +0200 Subject: [PATCH 107/359] #636 - un subclassed loader from sequence, created lib for reusable methods Loader from sequence didnt was dropping connection to PS after first successful import --- openpype/hosts/photoshop/plugins/lib.py | 26 +++++++++++++++ .../photoshop/plugins/load/load_image.py | 33 ++++--------------- .../plugins/load/load_image_from_sequence.py | 15 ++++++--- 3 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/lib.py diff --git a/openpype/hosts/photoshop/plugins/lib.py b/openpype/hosts/photoshop/plugins/lib.py new file mode 100644 index 0000000000..74aff06114 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/lib.py @@ -0,0 +1,26 @@ +import re + + +def get_unique_layer_name(layers, asset_name, subset_name): + """ + Gets all layer names and if 'asset_name_subset_name' is present, it + increases suffix by 1 (eg. creates unique layer name - for Loader) + Args: + layers (list) of dict with layers info (name, id etc.) + asset_name (string): + subset_name (string): + + Returns: + (string): name_00X (without version) + """ + name = "{}_{}".format(asset_name, subset_name) + names = {} + for layer in layers: + layer_name = re.sub(r'_\d{3}$', '', layer.name) + if layer_name in names.keys(): + names[layer_name] = names[layer_name] + 1 + else: + names[layer_name] = 1 + occurrences = names.get(name, 0) + + return "{}_{:0>3d}".format(name, occurrences + 1) diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index 44cc96c96f..d043323768 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -1,7 +1,9 @@ -from avalon import api, photoshop -import os import re +from avalon import api, photoshop + +from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name + stub = photoshop.stub() @@ -15,8 +17,9 @@ class ImageLoader(api.Loader): representations = ["*"] def load(self, context, name=None, namespace=None, data=None): - layer_name = self._get_unique_layer_name(context["asset"]["name"], - name) + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"]["name"], + name) with photoshop.maintained_selection(): layer = stub.import_smart_object(self.fname, layer_name) @@ -69,25 +72,3 @@ class ImageLoader(api.Loader): def switch(self, container, representation): self.update(container, representation) - - def _get_unique_layer_name(self, asset_name, subset_name): - """ - Gets all layer names and if 'name' is present in them, increases - suffix by 1 (eg. creates unique layer name - for Loader) - Args: - name (string): in format asset_subset - - Returns: - (string): name_00X (without version) - """ - name = "{}_{}".format(asset_name, subset_name) - names = {} - for layer in stub.get_layers(): - layer_name = re.sub(r'_\d{3}$', '', layer.name) - if layer_name in names.keys(): - names[layer_name] = names[layer_name] + 1 - else: - names[layer_name] = 1 - occurrences = names.get(name, 0) - - return "{}_{:0>3d}".format(name, occurrences + 1) diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 1409065d43..09525d2791 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -1,13 +1,15 @@ import os + +from avalon import api from avalon import photoshop from avalon.vendor import qargparse -from openpype.hosts.photoshop.plugins.load.load_image import ImageLoader +from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name stub = photoshop.stub() -class ImageFromSequenceLoader(ImageLoader): +class ImageFromSequenceLoader(api.Loader): """ Load specifing image from sequence Used only as quick load of reference file from a sequence. @@ -36,8 +38,11 @@ class ImageFromSequenceLoader(ImageLoader): if not os.path.exists(self.fname): return - layer_name = self._get_unique_layer_name(context["asset"]["name"], - name) + stub = photoshop.stub() + layer_name = get_unique_layer_name(stub.get_layers(), + context["asset"]["name"], + name) + with photoshop.maintained_selection(): layer = stub.import_smart_object(self.fname, layer_name) @@ -59,7 +64,7 @@ class ImageFromSequenceLoader(ImageLoader): """ files = [] for context in repre_contexts: - fname = ImageLoader.filepath_from_context(context) + fname = ImageFromSequenceLoader.filepath_from_context(context) _, file_extension = os.path.splitext(fname) for file_name in os.listdir(os.path.dirname(fname)): From 8813c7cc062742ee27a75726d08bc07b2c2413ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 13:11:39 +0200 Subject: [PATCH 108/359] #680 - added advanced_filtering to schema --- .../schema_project_ftrack.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index af85ccb254..a94ebc8888 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -651,6 +651,29 @@ "key": "add_ftrack_family", "label": "Add Ftrack Family", "type": "boolean" + }, + { + "type": "list", + "collapsible": true, + "key": "advanced_filtering", + "label": "Advanced adding if additional families present", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Additional Families", + "type": "list", + "object_type": "text" + }, + { + "key": "add_ftrack_family", + "label": "Add Ftrack Family", + "type": "boolean" + } + ] + } } ] } From 186bd21edc47af44745b6fedecbc143dba39dce6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 16:16:44 +0200 Subject: [PATCH 109/359] #680 - added documentation --- .../assets/ftrack/ftrack-collect-advanced.png | Bin 0 -> 16622 bytes .../assets/ftrack/ftrack-collect-main.png | Bin 0 -> 40169 bytes website/docs/module_ftrack.md | 28 +++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 website/docs/assets/ftrack/ftrack-collect-advanced.png create mode 100644 website/docs/assets/ftrack/ftrack-collect-main.png diff --git a/website/docs/assets/ftrack/ftrack-collect-advanced.png b/website/docs/assets/ftrack/ftrack-collect-advanced.png new file mode 100644 index 0000000000000000000000000000000000000000..5249685c86d21461589b6556a5453becf0d742ae GIT binary patch literal 16622 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 C%`C70 literal 0 HcmV?d00001 diff --git a/website/docs/assets/ftrack/ftrack-collect-main.png b/website/docs/assets/ftrack/ftrack-collect-main.png new file mode 100644 index 0000000000000000000000000000000000000000..7c75cd6269e8862845f392dea0b98852b4dd4bff GIT binary patch literal 40169 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 Ch7%9~ literal 0 HcmV?d00001 diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index bd0dbaef4f..c3b9fd6bc2 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -196,7 +196,7 @@ Is used to remove value from `Avalon/Mongo Id` Custom Attribute when entity is c ### Sync status from Task to Parent -List of parent boject types where this is triggered ("Shot", "Asset build", etc. Skipped if it is empty) +List of parent object types where this is triggered ("Shot", "Asset build", etc. Skipped if it is empty) ### Sync status from Version to Task @@ -214,3 +214,29 @@ This is usefull for example if first version publish doesn't contain any actual ### Update status on next task Change status on next task by task types order when task status state changed to "Done". All tasks with the same Task mapping of next task status changes From → To. Some status can be ignored. + +## Publish plugins + +### Collect Ftrack Family + +Reviews uploads to Ftrack could be configured by combination of hosts, families and task names. +(Currently implemented only in Standalone Publisher, Maya.) + +#### Profiles + +Profiles are used to select when to add Ftrack family to the instance. One or multiple profiles could be configured, Families, Task names (regex available), Host names combination is needed. + +Eg. If I want review created and uploaded to Ftrack for render published from Maya , setting is: + +Host names: 'Maya' +Families: 'render' +Add Ftrack Family: enabled + +![Collect Ftrack Family](assets/ftrack/ftrack-collect-main.png) + +#### Advanced adding if additional families present + +In special cases adding 'ftrack' based on main family ('Families' set higher) is not enough. +(For example upload to Ftrack for 'plate' main family should only happen if 'review' is contained in instance 'families', not added in other cases. ) + +![Collect Ftrack Family](assets/ftrack/ftrack-collect-advanced.png) \ No newline at end of file From ba5d6a17d23439d98094792075f65e7c29345897 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Jun 2021 16:17:06 +0200 Subject: [PATCH 110/359] Fixed wrong label --- website/docs/admin_hosts_aftereffects.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_aftereffects.md b/website/docs/admin_hosts_aftereffects.md index dc43820465..e3a09058da 100644 --- a/website/docs/admin_hosts_aftereffects.md +++ b/website/docs/admin_hosts_aftereffects.md @@ -14,7 +14,7 @@ All of them are Project based, eg. each project could have different configurati Location: Settings > Project > AfterEffects -![Harmony Project Settings](assets/admin_hosts_aftereffects_settings.png) +![AfterEffects Project Settings](assets/admin_hosts_aftereffects_settings.png) ## Publish plugins From 235059f9cdcae4fadad3f1c90c10c1636618dc13 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 19 Jun 2021 03:47:04 +0000 Subject: [PATCH 111/359] [Automated] Bump version --- CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++++++++++++- openpype/version.py | 2 +- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 043295eb8c..d7c0a25c7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,59 @@ # Changelog +## [3.2.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...HEAD) + +**🚀 Enhancements** + +- Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) +- Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) +- Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) +- Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699) +- Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695) + +**🐛 Bug fixes** + +- TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) +- Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) +- Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) +- Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) + +## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-18) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.2...2.18.3) + +**🐛 Bug fixes** + +- Remove publish highlight icon in AE [\#1664](https://github.com/pypeclub/OpenPype/pull/1664) + +**Merged pull requests:** + +- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) +- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) +- \#683 - Validate frame range in Standalone Publisher [\#1680](https://github.com/pypeclub/OpenPype/pull/1680) +- Maya: Split model content validator [\#1654](https://github.com/pypeclub/OpenPype/pull/1654) + +## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2) + +**🚀 Enhancements** + +- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650) + +**🐛 Bug fixes** + +- Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713) +- StandalonePublisher: instance data attribute `keepSequence` [\#1668](https://github.com/pypeclub/OpenPype/pull/1668) + +**Merged pull requests:** + +- 1698 Nuke: Prerender Frame Range by default [\#1709](https://github.com/pypeclub/OpenPype/pull/1709) + ## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.0.0...3.1.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.1.0-nightly.4...3.1.0) **🚀 Enhancements** @@ -33,6 +84,7 @@ - Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) - Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) +- Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) - New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 4312333660..ece0359506 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.1.0" +__version__ = "3.2.0-nightly.1" From 582f228d0a7fd1d0748bf7a7862b55611e069b32 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 10:54:22 +0200 Subject: [PATCH 112/359] #680 - small fixes --- .../ftrack/plugins/publish/collect_ftrack_family.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index 8dbcb0ab2c..b505a429b5 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -34,11 +34,9 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): self.log.warning("No profiles present for adding Ftrack family") return - anatomy_data = instance.context.data["anatomyData"] task_name = instance.data.get("task", avalon.api.Session["AVALON_TASK"]) - host_name = anatomy_data.get("app", - avalon.api.Session["AVALON_APP"]) + host_name = avalon.api.Session["AVALON_APP"] family = instance.data["family"] filtering_criteria = { @@ -46,7 +44,8 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): "families": family, "tasks": task_name } - profile = filter_profiles(self.profiles, filtering_criteria) + profile = filter_profiles(self.profiles, filtering_criteria, + logger=self.log) if profile: families = instance.data.get("families") @@ -93,7 +92,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): override_filter_value = -1 for additional_filter in additional_filters: filter_families = set(additional_filter["families"]) - valid = filter_families <= families # issubset + valid = filter_families <= set(families) # issubset if not valid: continue From 10f5c76973ba97929ea01a10c12cb01ce3324930 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 11:07:07 +0200 Subject: [PATCH 113/359] #680 - allow configuration for editorial --- .../plugins/publish/collect_instances.py | 4 +-- .../defaults/project_settings/ftrack.json | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py index eb04217136..d753a3d9bb 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py @@ -16,12 +16,12 @@ class CollectInstances(pyblish.api.InstancePlugin): subsets = { "referenceMain": { "family": "review", - "families": ["clip", "ftrack"], + "families": ["clip"], "extensions": [".mp4"] }, "audioMain": { "family": "audio", - "families": ["clip", "ftrack"], + "families": ["clip"], "extensions": [".wav"], }, "shotMain": { diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f83b6cef3a..f43900c76f 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -204,22 +204,30 @@ "enabled": true, "profiles": [ { + "hosts": [ + "standalonepublisher" + ], "families": [], "tasks": [], + "add_ftrack_family": true, + "advanced_filtering": [] + }, + { "hosts": [ "standalonepublisher" ], - "add_ftrack_family": true - }, - { - "families": ["matchmove"], + "families": [ + "matchmove", + "shot" + ], "tasks": [], - "hosts": [ - "standalonepublisher" - ], - "add_ftrack_family": false + "add_ftrack_family": false, + "advanced_filtering": [] }, { + "hosts": [ + "maya" + ], "families": [ "model", "setdress", @@ -229,10 +237,8 @@ "camera" ], "tasks": [], - "hosts": [ - "maya" - ], - "add_ftrack_family": true + "add_ftrack_family": true, + "advanced_filtering": [] } ] }, From 9a35e2d4f8629d923c31ade1a83440150220d3e1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 15:51:27 +0200 Subject: [PATCH 114/359] don't skip processing if mapping is not set and use default mapping --- .../event_version_to_task_statuses.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py index d20e2ff5a8..f215bedcc2 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py +++ b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py @@ -66,15 +66,7 @@ class VersionToTaskStatus(BaseEvent): )) return - _status_mapping = event_settings["mapping"] - if not _status_mapping: - self.log.debug( - "Project \"{}\" does not have set mapping for {}".format( - project_name, self.__class__.__name__ - ) - ) - return - + _status_mapping = event_settings["mapping"] or {} status_mapping = { key.lower(): value for key, value in _status_mapping.items() From aea7d538095a794e44ce50d6e9074b7d6715e2f4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 21 Jun 2021 17:36:51 +0200 Subject: [PATCH 115/359] #636 - use new api function --- .../hosts/photoshop/plugins/load/load_image_from_sequence.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py index 09525d2791..8704627b12 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -2,8 +2,10 @@ import os from avalon import api from avalon import photoshop +from avalon.pipeline import get_representation_path_from_context from avalon.vendor import qargparse +from openpype.lib import Anatomy from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name stub = photoshop.stub() @@ -64,7 +66,7 @@ class ImageFromSequenceLoader(api.Loader): """ files = [] for context in repre_contexts: - fname = ImageFromSequenceLoader.filepath_from_context(context) + fname = get_representation_path_from_context(context) _, file_extension = os.path.splitext(fname) for file_name in os.listdir(os.path.dirname(fname)): @@ -93,3 +95,4 @@ class ImageFromSequenceLoader(api.Loader): def remove(self, container): """No update possible, not containerized.""" pass + From 0e30fd8e74e002042aa6efb00e032267e6167882 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 18:52:19 +0200 Subject: [PATCH 116/359] defined ProjectHandler which cares about project model refresh and current project --- openpype/tools/launcher/lib.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/tools/launcher/lib.py b/openpype/tools/launcher/lib.py index b4e6a0c3e9..6b02e684d6 100644 --- a/openpype/tools/launcher/lib.py +++ b/openpype/tools/launcher/lib.py @@ -15,7 +15,7 @@ provides a bridge between the file-based project inventory and configuration. """ import os -from Qt import QtGui +from Qt import QtGui, QtCore from avalon.vendor import qtawesome from openpype.api import resources @@ -23,6 +23,24 @@ ICON_CACHE = {} NOT_FOUND = type("NotFound", (object, ), {}) +class ProjectHandler(QtCore.QObject): + project_changed = QtCore.Signal(str) + + def __init__(self, dbcon, model): + super(ProjectHandler, self).__init__() + self.current_project = dbcon.Session.get("AVALON_PROJECT") + self.model = model + self.dbcon = dbcon + + def set_project(self, project_name): + self.current_project = project_name + self.dbcon.Session["AVALON_PROJECT"] = project_name + self.project_changed.emit(project_name) + + def refresh_model(self): + self.model.refresh() + + def get_action_icon(action): icon_name = action.icon if not icon_name: From e63b94351980750a76cc9b4c4ad51929f2faba95 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:07:30 +0200 Subject: [PATCH 117/359] use ProjectHandler in main window --- openpype/tools/launcher/window.py | 34 ++++++++++++------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index a6d34bbe9d..8f8af9880a 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -12,7 +12,7 @@ from avalon.tools import lib as tools_lib from avalon.tools.widgets import AssetWidget from avalon.vendor import qtawesome from .models import ProjectModel -from .lib import get_action_label +from .lib import get_action_label, ProjectHandler from .widgets import ( ProjectBar, ActionBar, @@ -321,8 +321,12 @@ class LauncherWindow(QtWidgets.QDialog): self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint ) - project_panel = ProjectsPanel(self.dbcon) - asset_panel = AssetsPanel(self.dbcon) + project_model = ProjectModel(self.dbcon) + project_model.hide_invisible = True + project_handler = ProjectHandler(self.dbcon, project_model) + + project_panel = ProjectsPanel(project_handler) + asset_panel = AssetsPanel(project_handler, self.dbcon) page_slider = SlidePageWidget() page_slider.addWidget(project_panel) @@ -371,6 +375,8 @@ class LauncherWindow(QtWidgets.QDialog): actions_refresh_timer.setInterval(self.actions_refresh_timeout) self.actions_refresh_timer = actions_refresh_timer + self.project_handler = project_handler + self.message_label = message_label self.project_panel = project_panel self.asset_panel = asset_panel @@ -383,15 +389,10 @@ class LauncherWindow(QtWidgets.QDialog): actions_refresh_timer.timeout.connect(self._on_action_timer) actions_bar.action_clicked.connect(self.on_action_clicked) action_history.trigger_history.connect(self.on_history_action) - project_panel.project_clicked.connect(self.on_project_clicked) + project_handler.project_changed.connect(self.on_project_change) asset_panel.back_clicked.connect(self.on_back_clicked) asset_panel.session_changed.connect(self.on_session_changed) - # todo: Simplify this callback connection - asset_panel.project_bar.project_changed.connect( - self.on_project_changed - ) - self.resize(520, 740) def showEvent(self, event): @@ -415,13 +416,6 @@ class LauncherWindow(QtWidgets.QDialog): QtCore.QTimer.singleShot(5000, lambda: self.message_label.setText("")) self.log.debug(message) - def on_project_changed(self): - project_name = self.asset_panel.project_bar.get_current_project() - self.dbcon.Session["AVALON_PROJECT"] = project_name - - # Update the Action plug-ins available for the current project - self.discover_actions() - def on_session_changed(self): self.filter_actions() @@ -441,15 +435,13 @@ class LauncherWindow(QtWidgets.QDialog): # Refresh projects if window is active self.discover_actions() - def on_project_clicked(self, project_name): - self.dbcon.Session["AVALON_PROJECT"] = project_name - # Refresh projects - self.asset_panel.set_project(project_name) + def on_project_change(self, project_name): + # Update the Action plug-ins available for the current project self.set_page(1) self.discover_actions() def on_back_clicked(self): - self.dbcon.Session["AVALON_PROJECT"] = None + self.project_handler.set_project(None) self.set_page(0) self.discover_actions() From 480900a1cc0c6095474f9879d2386cc4745ba24b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:08:35 +0200 Subject: [PATCH 118/359] use ProjectHandler object instead of passing dbcon changes and simplified project handling in Launcher tool --- openpype/tools/launcher/widgets.py | 40 ++++++++++++------------ openpype/tools/launcher/window.py | 50 +++++++----------------------- 2 files changed, 32 insertions(+), 58 deletions(-) diff --git a/openpype/tools/launcher/widgets.py b/openpype/tools/launcher/widgets.py index 0e8caeb278..048210115c 100644 --- a/openpype/tools/launcher/widgets.py +++ b/openpype/tools/launcher/widgets.py @@ -20,25 +20,15 @@ from .constants import ( class ProjectBar(QtWidgets.QWidget): - project_changed = QtCore.Signal(int) - - # Project list will be refreshed each 10000 msecs - refresh_interval = 10000 - - def __init__(self, dbcon, parent=None): + def __init__(self, project_handler, parent=None): super(ProjectBar, self).__init__(parent) - self.dbcon = dbcon - - model = ProjectModel(dbcon) - model.hide_invisible = True - project_combobox = QtWidgets.QComboBox(self) # Change delegate so stylysheets are applied project_delegate = QtWidgets.QStyledItemDelegate(project_combobox) project_combobox.setItemDelegate(project_delegate) - project_combobox.setModel(model) + project_combobox.setModel(project_handler.model) project_combobox.setRootModelIndex(QtCore.QModelIndex()) layout = QtWidgets.QHBoxLayout(self) @@ -51,19 +41,20 @@ class ProjectBar(QtWidgets.QWidget): ) refresh_timer = QtCore.QTimer() - refresh_timer.setInterval(self.refresh_interval) + refresh_timer.setInterval(project_handler.refresh_interval) - self.model = model + self.project_handler = project_handler self.project_delegate = project_delegate self.project_combobox = project_combobox self.refresh_timer = refresh_timer # Signals refresh_timer.timeout.connect(self._on_refresh_timeout) - self.project_combobox.currentIndexChanged.connect(self.project_changed) + self.project_combobox.currentIndexChanged.connect(self.on_index_change) + project_handler.project_changed.connect(self._on_project_change) # Set current project by default if it's set. - project_name = self.dbcon.Session.get("AVALON_PROJECT") + project_name = project_handler.current_project if project_name: self.set_project(project_name) @@ -79,7 +70,12 @@ class ProjectBar(QtWidgets.QWidget): elif self.isActiveWindow(): # Refresh projects if window is active - self.model.refresh() + self.project_handler.refresh_model() + + def _on_project_change(self, project_name): + if self.get_current_project() == project_name: + return + self.set_project(project_name) def get_current_project(self): return self.project_combobox.currentText() @@ -88,14 +84,18 @@ class ProjectBar(QtWidgets.QWidget): index = self.project_combobox.findText(project_name) if index < 0: # Try refresh combobox model - self.refresh() + self.project_handler.refresh_model() index = self.project_combobox.findText(project_name) if index >= 0: self.project_combobox.setCurrentIndex(index) - def refresh(self): - self.model.refresh() + def on_index_change(self, idx): + if not self.isVisible(): + return + + project_name = self.get_current_project() + self.project_handler.set_project(project_name) class ActionBar(QtWidgets.QWidget): diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index 8f8af9880a..d9f9f5a8f3 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -89,46 +89,37 @@ class ProjectIconView(QtWidgets.QListView): class ProjectsPanel(QtWidgets.QWidget): """Projects Page""" - - project_clicked = QtCore.Signal(str) - # Refresh projects each 10000 msecs - refresh_interval = 10000 - - def __init__(self, dbcon, parent=None): + def __init__(self, project_handler, parent=None): super(ProjectsPanel, self).__init__(parent=parent) layout = QtWidgets.QVBoxLayout(self) - self.dbcon = dbcon - self.dbcon.install() - view = ProjectIconView(parent=self) view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(view) - model = ProjectModel(self.dbcon) - model.hide_invisible = True - view.setModel(model) + + view.setModel(project_handler.model) layout.addWidget(view) refresh_timer = QtCore.QTimer() - refresh_timer.setInterval(self.refresh_interval) + refresh_timer.setInterval(project_handler.refresh_interval) refresh_timer.timeout.connect(self._on_refresh_timeout) view.clicked.connect(self.on_clicked) - self.model = model self.view = view self.refresh_timer = refresh_timer + self.project_handler = project_handler def on_clicked(self, index): if index.isValid(): project_name = index.data(QtCore.Qt.DisplayRole) - self.project_clicked.emit(project_name) + self.project_handler.set_project(project_name) def showEvent(self, event): - self.model.refresh() + self.project_handler.refresh_model() if not self.refresh_timer.isActive(): self.refresh_timer.start() super(ProjectsPanel, self).showEvent(event) @@ -140,7 +131,7 @@ class ProjectsPanel(QtWidgets.QWidget): elif self.isActiveWindow(): # Refresh projects if window is active - self.model.refresh() + self.project_handler.refresh_model() class AssetsPanel(QtWidgets.QWidget): @@ -148,7 +139,7 @@ class AssetsPanel(QtWidgets.QWidget): back_clicked = QtCore.Signal() session_changed = QtCore.Signal() - def __init__(self, dbcon, parent=None): + def __init__(self, project_handler, dbcon, parent=None): super(AssetsPanel, self).__init__(parent=parent) self.dbcon = dbcon @@ -163,7 +154,7 @@ class AssetsPanel(QtWidgets.QWidget): btn_back = QtWidgets.QPushButton(project_bar_widget) btn_back.setIcon(btn_back_icon) - project_bar = ProjectBar(self.dbcon, project_bar_widget) + project_bar = ProjectBar(project_handler, project_bar_widget) layout.addWidget(btn_back) layout.addWidget(project_bar) @@ -206,24 +197,19 @@ class AssetsPanel(QtWidgets.QWidget): layout.addWidget(body) # signals - project_bar.project_changed.connect(self.on_project_changed) + project_handler.project_changed.connect(self.on_project_changed) assets_widget.selection_changed.connect(self.on_asset_changed) assets_widget.refreshed.connect(self.on_asset_changed) tasks_widget.task_changed.connect(self.on_task_change) btn_back.clicked.connect(self.back_clicked) + self.project_handler = project_handler self.project_bar = project_bar self.assets_widget = assets_widget self.tasks_widget = tasks_widget self._btn_back = btn_back - # Force initial refresh for the assets since we might not be - # trigging a Project switch if we click the project that was set - # prior to launching the Launcher - # todo: remove this behavior when AVALON_PROJECT is not required - assets_widget.refresh() - def showEvent(self, event): super(AssetsPanel, self).showEvent(event) @@ -232,19 +218,7 @@ class AssetsPanel(QtWidgets.QWidget): btn_size = self.project_bar.height() self._btn_back.setFixedSize(QtCore.QSize(btn_size, btn_size)) - def set_project(self, project): - before = self.project_bar.get_current_project() - if before == project: - self.assets_widget.refresh() - return - - self.project_bar.set_project(project) - self.on_project_changed() - def on_project_changed(self): - project_name = self.project_bar.get_current_project() - self.dbcon.Session["AVALON_PROJECT"] = project_name - self.session_changed.emit() self.assets_widget.refresh() From 072718542fd73365afae1d51f0f22a8689031621 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:08:42 +0200 Subject: [PATCH 119/359] don't print asset change --- openpype/tools/launcher/window.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/tools/launcher/window.py b/openpype/tools/launcher/window.py index d9f9f5a8f3..979aab42cf 100644 --- a/openpype/tools/launcher/window.py +++ b/openpype/tools/launcher/window.py @@ -227,11 +227,8 @@ class AssetsPanel(QtWidgets.QWidget): """Callback on asset selection changed This updates the task view. - """ - print("Asset changed..") - asset_name = None asset_silo = None From 23e2fd54d8f140fa2c8555431ae79bac012f7830 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:08:53 +0200 Subject: [PATCH 120/359] added few comments and docstrings --- openpype/tools/launcher/lib.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/tools/launcher/lib.py b/openpype/tools/launcher/lib.py index 6b02e684d6..65d40cd0df 100644 --- a/openpype/tools/launcher/lib.py +++ b/openpype/tools/launcher/lib.py @@ -24,17 +24,44 @@ NOT_FOUND = type("NotFound", (object, ), {}) class ProjectHandler(QtCore.QObject): + """Handler of project model and current project in Launcher tool. + + Helps to organize two separate widgets handling current project selection. + + It is easier to trigger project change callbacks from one place than from + multiple differect places without proper handling or sequence changes. + + Args: + dbcon(AvalonMongoDB): Mongo connection with Session. + model(ProjectModel): Object of projects model which is shared across + all widgets using projects. Arg dbcon should be used as source for + the model. + """ + # Project list will be refreshed each 10000 msecs + # - this is not part of helper implementation but should be used by widgets + # that may require reshing of projects + refresh_interval = 10000 + + # Signal emmited when project has changed project_changed = QtCore.Signal(str) def __init__(self, dbcon, model): super(ProjectHandler, self).__init__() - self.current_project = dbcon.Session.get("AVALON_PROJECT") + # Store project model for usage self.model = model + # Store dbcon self.dbcon = dbcon + self.current_project = dbcon.Session.get("AVALON_PROJECT") + def set_project(self, project_name): + # Change current project of this handler self.current_project = project_name + # Change session project to take effect for other widgets using the + # dbcon object. self.dbcon.Session["AVALON_PROJECT"] = project_name + + # Trigger change signal when everything is updated to new project self.project_changed.emit(project_name) def refresh_model(self): From 46f284ed15a4bdaf643f7bb9525f340b2117b0a9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:37:57 +0200 Subject: [PATCH 121/359] Show a missing attribute as `<` are `>` used as html tags --- openpype/modules/ftrack/lib/avalon_sync.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 5d1da005dc..85c8d5a273 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -402,9 +402,9 @@ class SyncEntitiesFactory: items = [] items.append({ "type": "label", - "value": "# Can't access Custom attribute <{}>".format( - CUST_ATTR_ID_KEY - ) + "value": ( + "# Can't access Custom attribute: \"{}\"" + ).format(CUST_ATTR_ID_KEY) }) items.append({ "type": "label", From ca26f56c2662cb70d1b611eb963c52594e6d317e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 19:38:02 +0200 Subject: [PATCH 122/359] better message --- openpype/modules/ftrack/lib/avalon_sync.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py index 85c8d5a273..2458308af5 100644 --- a/openpype/modules/ftrack/lib/avalon_sync.py +++ b/openpype/modules/ftrack/lib/avalon_sync.py @@ -409,9 +409,11 @@ class SyncEntitiesFactory: items.append({ "type": "label", "value": ( - "

- Check if user \"{}\" has permissions" - " to access the Custom attribute

" - ).format(self._api_key) + "

- Check if your User and API key has permissions" + " to access the Custom attribute." + "
Username:\"{}\"" + "
API key:\"{}\"

" + ).format(self._api_user, self._api_key) }) items.append({ "type": "label", From bf284caccf998352222722af616287e4fd411c16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 21 Jun 2021 20:03:31 +0200 Subject: [PATCH 123/359] make sure bg compositing is happening only if alpha is available --- .../plugins/publish/extract_sequence.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 007b5c41f1..8588a5b07c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -293,10 +293,15 @@ class ExtractSequence(pyblish.api.Extractor): thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") if first_frame_filepath and os.path.exists(first_frame_filepath): + # Composite background only on rgba images + # - just making sure source_img = Image.open(first_frame_filepath) - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) + if source_img.mode.lower() == "rgba": + bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + thumbnail_obj = Image.alpha_composite(bg_image, source_img) + thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -392,9 +397,14 @@ class ExtractSequence(pyblish.api.Extractor): if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath): source_img = Image.open(thumbnail_src_filepath) thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") - thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255)) - thumbnail_obj.paste(source_img) - thumbnail_obj.save(thumbnail_filepath) + # Composite background only on rgba images + # - just making sure + if source_img.mode.lower() == "rgba": + bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + thumbnail_obj = Image.alpha_composite(bg_image, source_img) + thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath From b9861d48dee01e8cd31d28a08a1840c0664be12d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 10:40:02 +0200 Subject: [PATCH 124/359] added settings for ValidateContainers into each host --- .../projects_schema/schema_project_harmony.json | 10 ++++++++++ .../projects_schema/schema_project_photoshop.json | 12 +++++++++++- .../projects_schema/schemas/schema_maya_publish.json | 11 ++++++++++- .../projects_schema/schemas/schema_nuke_publish.json | 10 ++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json index 8b5d638cd8..ce6246a8de 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_harmony.json @@ -25,6 +25,16 @@ } ] }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, { "type": "dict", "collapsible": 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 4eb6c26dbb..3b65f08ac4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_photoshop.json @@ -33,6 +33,16 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, { "type": "dict", "collapsible": true, @@ -50,7 +60,7 @@ "object_type": "text" } ] - } + } ] }, { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 0abcdd2965..5ca7059ee5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -28,7 +28,16 @@ "type": "label", "label": "Validators" }, - + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 6873ed5190..782179cfd1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -29,6 +29,16 @@ "type": "label", "label": "Validators" }, + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + }, { "type": "dict", "collapsible": true, From cc79caa90fa63efa6d96e6858bc5c4705d1e14f3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 10:40:13 +0200 Subject: [PATCH 125/359] added settings for houdini --- .../schemas/projects_schema/schema_main.json | 4 +++ .../schema_project_houdini.json | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index bee9712878..4a8a9d496e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -82,6 +82,10 @@ "type": "schema", "name": "schema_project_hiero" }, + { + "type": "schema", + "name": "schema_project_houdini" + }, { "type": "schema", "name": "schema_project_blender" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json new file mode 100644 index 0000000000..c6de257a61 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -0,0 +1,27 @@ +{ + "type": "dict", + "collapsible": true, + "key": "houdini", + "label": "Houdini", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "key": "ValidateContainers", + "label": "ValidateContainers" + } + ] + } + ] + } + ] +} From 70176a3de10778697993f3e82b019888b5ecdf14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 10:40:37 +0200 Subject: [PATCH 126/359] saved defaults for ValidateContainers plugin --- openpype/settings/defaults/project_settings/harmony.json | 5 +++++ openpype/settings/defaults/project_settings/houdini.json | 9 +++++++++ openpype/settings/defaults/project_settings/maya.json | 5 +++++ openpype/settings/defaults/project_settings/nuke.json | 5 +++++ .../settings/defaults/project_settings/photoshop.json | 5 +++++ 5 files changed, 29 insertions(+) create mode 100644 openpype/settings/defaults/project_settings/houdini.json diff --git a/openpype/settings/defaults/project_settings/harmony.json b/openpype/settings/defaults/project_settings/harmony.json index 0c7a35c058..3cc175ae72 100644 --- a/openpype/settings/defaults/project_settings/harmony.json +++ b/openpype/settings/defaults/project_settings/harmony.json @@ -5,6 +5,11 @@ ".*" ] }, + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateSceneSettings": { "enabled": true, "optional": true, diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json new file mode 100644 index 0000000000..811a446e59 --- /dev/null +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -0,0 +1,9 @@ +{ + "publish": { + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + } + } +} \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index ba685ae502..284a1a0040 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -127,6 +127,11 @@ "CollectMayaRender": { "sync_workfile_version": false }, + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateShaderName": { "enabled": false, "regex": "(?P.*)_(.*)_SHD" diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 6ff732634e..71bf46d5b3 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -21,6 +21,11 @@ "PreCollectNukeInstances": { "sync_workfile_version": true }, + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, "ValidateKnobs": { "enabled": false, "knobs": { diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index b306a757a6..4c36e4bd49 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -7,6 +7,11 @@ } }, "publish": { + "ValidateContainers": { + "enabled": true, + "optional": true, + "active": true + }, "ExtractImage": { "formats": [ "png", From 79918fdf17dec2b6952ea6daa6a96f053fb98242 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:34:59 +0200 Subject: [PATCH 127/359] show error dialog if loading or updating of local settings crash --- .../tools/settings/local_settings/window.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index 69562d0b1f..f1020a0764 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -203,8 +203,40 @@ class LocalSettingsWindow(QtWidgets.QWidget): def reset(self): if self._reset_on_show: self._reset_on_show = False - value = get_local_settings() - self.settings_widget.update_local_settings(value) + + error_msg = None + try: + value = get_local_settings() + self.settings_widget.update_local_settings(value) + + except Exception as exc: + error_msg = str(exc) + + crashed = error_msg is not None + # Enable/Disable save button if crashed or not + self.save_btn.setEnabled(not crashed) + # Show/Hide settings widget if crashed or not + if self.settings_widget: + self.settings_widget.setVisible(not crashed) + + if not crashed: + return + + # Show message with error + title = "Something went wrong" + msg = ( + "This is probably a bug. Loading of settings failed." + "\n\nError message:\n{}" + ).format(error_msg) + + dialog = QtWidgets.QMessageBox( + QtWidgets.QMessageBox.Warning, + title, + msg, + QtWidgets.QMessageBox.Ok, + self + ) + dialog.exec_() def _on_reset_clicked(self): self.reset() From 4c58e2aadca6a5a29b217a88e2e56d759120964b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:35:28 +0200 Subject: [PATCH 128/359] create LocalSettingsWidget on reset --- openpype/tools/settings/local_settings/window.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index f1020a0764..806b8840bd 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -168,9 +168,6 @@ class LocalSettingsWindow(QtWidgets.QWidget): scroll_widget = QtWidgets.QScrollArea(self) scroll_widget.setObjectName("GroupWidget") - settings_widget = LocalSettingsWidget(scroll_widget) - - scroll_widget.setWidget(settings_widget) scroll_widget.setWidgetResizable(True) footer = QtWidgets.QWidget(self) @@ -191,7 +188,8 @@ class LocalSettingsWindow(QtWidgets.QWidget): save_btn.clicked.connect(self._on_save_clicked) reset_btn.clicked.connect(self._on_reset_clicked) - self.settings_widget = settings_widget + self.settings_widget = None + self.scroll_widget = scroll_widget self.reset_btn = reset_btn self.save_btn = save_btn @@ -206,6 +204,10 @@ class LocalSettingsWindow(QtWidgets.QWidget): error_msg = None try: + if self.settings_widget is None: + self.settings_widget = LocalSettingsWidget(self.scroll_widget) + self.scroll_widget.setWidget(self.settings_widget) + value = get_local_settings() self.settings_widget.update_local_settings(value) From c0663c4e51a6657e69c907a76e549c955b63701d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:41:20 +0200 Subject: [PATCH 129/359] modified message and change icon --- openpype/tools/settings/local_settings/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index 806b8840bd..e0fe8092f2 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -227,12 +227,13 @@ class LocalSettingsWindow(QtWidgets.QWidget): # Show message with error title = "Something went wrong" msg = ( - "This is probably a bug. Loading of settings failed." + "Bug: Loading of settings failed." + " Please contact your project manager or OpenPype team." "\n\nError message:\n{}" ).format(error_msg) dialog = QtWidgets.QMessageBox( - QtWidgets.QMessageBox.Warning, + QtWidgets.QMessageBox.Critical, title, msg, QtWidgets.QMessageBox.Ok, From d5c94ab20ca8f28d387aa5fefccde2474300dc68 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:41:31 +0200 Subject: [PATCH 130/359] changed attribute names --- .../tools/settings/local_settings/window.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/local_settings/window.py b/openpype/tools/settings/local_settings/window.py index e0fe8092f2..9e8fd89b23 100644 --- a/openpype/tools/settings/local_settings/window.py +++ b/openpype/tools/settings/local_settings/window.py @@ -188,8 +188,12 @@ class LocalSettingsWindow(QtWidgets.QWidget): save_btn.clicked.connect(self._on_save_clicked) reset_btn.clicked.connect(self._on_reset_clicked) - self.settings_widget = None - self.scroll_widget = scroll_widget + # Do not create local settings widget in init phase as it's using + # settings objects that must be OK to be able create this widget + # - we want to show dialog if anything goes wrong + # - without reseting nothing is shown + self._settings_widget = None + self._scroll_widget = scroll_widget self.reset_btn = reset_btn self.save_btn = save_btn @@ -204,12 +208,15 @@ class LocalSettingsWindow(QtWidgets.QWidget): error_msg = None try: - if self.settings_widget is None: - self.settings_widget = LocalSettingsWidget(self.scroll_widget) - self.scroll_widget.setWidget(self.settings_widget) + # Create settings widget if is not created yet + if self._settings_widget is None: + self._settings_widget = LocalSettingsWidget( + self._scroll_widget + ) + self._scroll_widget.setWidget(self._settings_widget) value = get_local_settings() - self.settings_widget.update_local_settings(value) + self._settings_widget.update_local_settings(value) except Exception as exc: error_msg = str(exc) @@ -218,8 +225,8 @@ class LocalSettingsWindow(QtWidgets.QWidget): # Enable/Disable save button if crashed or not self.save_btn.setEnabled(not crashed) # Show/Hide settings widget if crashed or not - if self.settings_widget: - self.settings_widget.setVisible(not crashed) + if self._settings_widget: + self._settings_widget.setVisible(not crashed) if not crashed: return @@ -245,6 +252,6 @@ class LocalSettingsWindow(QtWidgets.QWidget): self.reset() def _on_save_clicked(self): - value = self.settings_widget.settings_value() + value = self._settings_widget.settings_value() save_local_settings(value) self.reset() From ba13064a654c42cf0157ba67a5d31d1312aaa5e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 11:42:05 +0200 Subject: [PATCH 131/359] local settings action won't trigger reset on first trigger --- openpype/modules/settings_action.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py index 1035dc0dcd..9db4a252bc 100644 --- a/openpype/modules/settings_action.py +++ b/openpype/modules/settings_action.py @@ -114,6 +114,7 @@ class LocalSettingsAction(PypeModule, ITrayAction): # Tray attributes self.settings_window = None + self._first_trigger = True def connect_with_modules(self, *_a, **_kw): return @@ -153,6 +154,9 @@ class LocalSettingsAction(PypeModule, ITrayAction): self.settings_window.raise_() self.settings_window.activateWindow() - # Reset content if was not visible - if not was_visible: + # Do not reset if it's first trigger of action + if self._first_trigger: + self._first_trigger = False + elif not was_visible: + # Reset content if was not visible self.settings_window.reset() From 4b707d4cb2b4781bb14325b9051a24496d29a5cb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 12:14:26 +0200 Subject: [PATCH 132/359] Settings: ftrack family for editorial standalonepublishing --- .../defaults/project_settings/ftrack.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f43900c76f..03ecf024a6 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -224,6 +224,26 @@ "add_ftrack_family": false, "advanced_filtering": [] }, + { + "hosts": [ + "standalonepublisher" + ], + "families": [ + "review", + "plate" + ], + "tasks": [], + "add_ftrack_family": false, + "advanced_filtering": [ + { + "families": [ + "clip", + "review" + ], + "add_ftrack_family": true + } + ] + }, { "hosts": [ "maya" From b1e716d5cfeb7600f21287b48d57168a5b308775 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 12:19:41 +0200 Subject: [PATCH 133/359] StandalonePublisher: failing collector for editorial --- .../standalonepublisher/plugins/publish/collect_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py index eb04217136..ad89abba63 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py @@ -55,7 +55,7 @@ class CollectInstances(pyblish.api.InstancePlugin): fps = plib.get_asset()["data"]["fps"] tracks = timeline.each_child( - descended_from_type=otio.schema.track.Track + descended_from_type=otio.schema.Track ) # get data from avalon @@ -92,7 +92,7 @@ class CollectInstances(pyblish.api.InstancePlugin): # Transitions are ignored, because Clips have the full frame # range. - if isinstance(clip, otio.schema.transition.Transition): + if isinstance(clip, otio.schema.Transition): continue # basic unique asset name From c807bef0cb1c028c15d67d08c2576faf406d8171 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:43:07 +0200 Subject: [PATCH 134/359] added `hosts-enum` entity for settings --- openpype/settings/entities/__init__.py | 2 + openpype/settings/entities/enum_entity.py | 52 +++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index f64ca1e98d..94eb819f2b 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -101,6 +101,7 @@ from .color_entity import ColorEntity from .enum_entity import ( BaseEnumEntity, EnumEntity, + HostsEnumEntity, AppsEnumEntity, ToolsEnumEntity, TaskTypeEnumEntity, @@ -153,6 +154,7 @@ __all__ = ( "BaseEnumEntity", "EnumEntity", + "HostsEnumEntity", "AppsEnumEntity", "ToolsEnumEntity", "TaskTypeEnumEntity", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 0b0575a255..add5c0298c 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -101,6 +101,58 @@ class EnumEntity(BaseEnumEntity): super(EnumEntity, self).schema_validations() +class HostsEnumEntity(BaseEnumEntity): + schema_types = ["hosts-enum"] + + def _item_initalization(self): + self.multiselection = self.schema_data.get("multiselection", True) + self.use_empty_value = self.schema_data.get( + "use_empty_value", not self.multiselection + ) + self.empty_label = ( + self.schema_data.get("empty_label") or "< without host >" + ) + + self.enum_items = [ + {"aftereffects": "aftereffects"}, + {"blender": "blender"}, + {"celaction": "celaction"}, + {"fusion": "fusion"}, + {"harmony": "harmony"}, + {"hiero": "hiero"}, + {"houdini": "houdini"}, + {"maya": "maya"}, + {"nuke": "nuke"}, + {"photoshop": "photoshop"}, + {"resolve": "resolve"}, + {"tvpaint": "tvpaint"}, + {"unreal": "unreal"} + ] + + if self.use_empty_value: + self.enum_items.insert(0, {"": self.empty_label}) + + valid_keys = set() + for item in self.enum_items or []: + valid_keys.add(tuple(item.keys())[0]) + + self.valid_keys = valid_keys + + if self.multiselection: + self.valid_value_types = (list, ) + self.value_on_not_set = [] + else: + for key in valid_keys: + if self.value_on_not_set is NOT_SET: + self.value_on_not_set = key + break + + self.valid_value_types = (STRING_TYPE, ) + + # GUI attribute + self.placeholder = self.schema_data.get("placeholder") + + class AppsEnumEntity(BaseEnumEntity): schema_types = ["apps-enum"] From c1bb5fec8f6ecf3689408653210fc081039d93d9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:43:56 +0200 Subject: [PATCH 135/359] use host-enum instead of list of strings for hosts --- .../projects_schema/schema_project_slack.json | 4 ++-- .../schemas/schema_global_publish.json | 12 ++++++------ .../projects_schema/schemas/schema_global_tools.json | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json index 58708776ca..170de7c8a2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_slack.json @@ -59,10 +59,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Host names", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "separator" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 8ca203e3bc..496635287f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -90,10 +90,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "splitter" @@ -358,10 +358,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "type": "splitter" @@ -492,10 +492,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json index 224389d42e..8c92a45a56 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_tools.json @@ -35,10 +35,10 @@ "object_type": "text" }, { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", @@ -75,10 +75,10 @@ "type": "dict", "children": [ { + "type": "hosts-enum", "key": "hosts", "label": "Hosts", - "type": "list", - "object_type": "text" + "multiselection": true }, { "key": "tasks", From 9d84a39e11653d6f88f774d94e0963816ea51943 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:44:18 +0200 Subject: [PATCH 136/359] replaced hosts enum in applications with hosts-enum --- .../template_host_unchangables.json | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json index e8b2a70076..c4d8d89209 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_unchangables.json @@ -14,25 +14,11 @@ "roles": ["developer"] }, { - "type": "enum", + "type": "hosts-enum", "key": "host_name", "label": "Host implementation", - "enum_items": [ - { "": "< without host >" }, - { "aftereffects": "aftereffects" }, - { "blender": "blender" }, - { "celaction": "celaction" }, - { "fusion": "fusion" }, - { "harmony": "harmony" }, - { "hiero": "hiero" }, - { "houdini": "houdini" }, - { "maya": "maya" }, - { "nuke": "nuke" }, - { "photoshop": "photoshop" }, - { "resolve": "resolve" }, - { "tvpaint": "tvpaint" }, - { "unreal": "unreal" } - ], + "multiselection": false, + "use_empty_value": true, "roles": ["developer"] } ] From fbcb46ac0922cc03c69262d709c765ba319d5803 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:51:44 +0200 Subject: [PATCH 137/359] adde hosts-enum to readme --- openpype/settings/entities/schemas/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 6c31b61f59..0ad13bfe1a 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -272,6 +272,22 @@ } ``` +### hosts-enum +- enumeration of available hosts +- multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) +- it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) +- to modify label of empty value set `"empty_label"` key with your label (Default: `< without host >`) +``` +{ + "key": "host", + "label": "Host name", + "type": "hosts-enum", + "multiselection": false, + "use_empty_value": true, + "empty_label": "N/A" +} +``` + ## Inputs for setting value using Pure inputs - these inputs also have required `"key"` - attribute `"label"` is required in few conditions From d7ab6bb7ae8e2c33adf247978f884552c808e10d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 12:57:19 +0200 Subject: [PATCH 138/359] added few comments --- openpype/settings/entities/enum_entity.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index add5c0298c..050f0038f7 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -102,6 +102,21 @@ class EnumEntity(BaseEnumEntity): class HostsEnumEntity(BaseEnumEntity): + """Enumeration of host names. + + Enum items are hardcoded in definition of the entity. + + Hosts enum can have defined empty value as valid option which is + represented by empty string. Schema key to set this option is + `use_empty_value` (true/false). And to set label of empty value set + `empty_label` (string). + + Enum can have single and multiselection. + + NOTE: + Host name is not the same as application name. Host name defines + implementation instead of application name. + """ schema_types = ["hosts-enum"] def _item_initalization(self): @@ -113,6 +128,7 @@ class HostsEnumEntity(BaseEnumEntity): self.schema_data.get("empty_label") or "< without host >" ) + # These are hardcoded there is not list of available host in OpenPype self.enum_items = [ {"aftereffects": "aftereffects"}, {"blender": "blender"}, From ca36bb2fb7bd4e554ab8014bf20ba6c64573d09f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:05:19 +0200 Subject: [PATCH 139/359] Added settings for ExtractSequence to be able modify color of thumbnail background --- .../defaults/project_settings/tvpaint.json | 8 ++++++++ .../projects_schema/schema_project_tvpaint.json | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index b4f3b315ec..25b7056ce1 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,5 +1,13 @@ { "publish": { + "ExtractSequence": { + "thumbnail_bg": [ + 255, + 255, + 255, + 255 + ] + }, "ValidateProjectSettings": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 6f90bb4263..1894384bb9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -11,6 +11,21 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "ExtractSequence", + "label": "ExtractSequence", + "is_group": true, + "children": [ + { + "type": "color", + "key": "thumbnail_bg", + "label": "Thumbnail BG color", + "use_alpha": false + } + ] + }, { "type": "schema_template", "name": "template_publish_plugin", From 4628b7627a492642f7cb086b46be683260bbd51d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:05:43 +0200 Subject: [PATCH 140/359] define default value in ExtractSequence fot thumbnail background color --- openpype/hosts/tvpaint/plugins/publish/extract_sequence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 8588a5b07c..e0ea207db3 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -13,6 +13,9 @@ class ExtractSequence(pyblish.api.Extractor): hosts = ["tvpaint"] families = ["review", "renderPass", "renderLayer"] + # Modifiable with settings + thumbnail_bg = [255, 255, 255, 255] + def process(self, instance): self.log.info( "* Processing instance \"{}\"".format(instance.data["label"]) From 4d484189ed71114602bf9ebd46e1f434bde33e77 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 13:13:51 +0200 Subject: [PATCH 141/359] added function to get thumbnail bg color --- .../plugins/publish/extract_sequence.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index e0ea207db3..c13066d2c3 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -300,10 +300,18 @@ class ExtractSequence(pyblish.api.Extractor): # - just making sure source_img = Image.open(first_frame_filepath) if source_img.mode.lower() == "rgba": - bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + bg_color = self._get_thumbnail_bg_color() + self.log.debug("Adding thumbnail background color {}.".format( + " ".join(bg_color) + )) + bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) thumbnail_obj.convert("RGB").save(thumbnail_filepath) else: + self.log.info(( + "Source for thumbnail has mode \"{}\" (Expected: RGBA)." + " Can't use thubmanail background color." + ).format(source_img.mode)) source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -403,14 +411,32 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure if source_img.mode.lower() == "rgba": - bg_image = Image.new("RGBA", source_img.size, (255, 255, 255)) + bg_color = self._get_thumbnail_bg_color() + self.log.debug("Adding thumbnail background color {}.".format( + " ".join(bg_color) + )) + bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) thumbnail_obj.convert("RGB").save(thumbnail_filepath) + else: + self.log.info(( + "Source for thumbnail has mode \"{}\" (Expected: RGBA)." + " Can't use thubmanail background color." + ).format(source_img.mode)) source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath + def _get_thumbnail_bg_color(self): + red = green = blue = 255 + if self.thumbnail_bg: + if len(self.thumbnail_bg) == 4: + red, green, blue, _ = self.thumbnail_bg + elif len(self.thumbnail_bg) == 3: + red, green, blue = self.thumbnail_bg + return (red, green, blue) + def _render_layer( self, layer, From a7962aa3a05769a14a8091a7445e63945656b900 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 14:09:48 +0200 Subject: [PATCH 142/359] modified how labels in hosts enum works --- openpype/settings/entities/enum_entity.py | 52 +++++++++++--------- openpype/settings/entities/schemas/README.md | 7 ++- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 050f0038f7..d197683afe 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -124,34 +124,38 @@ class HostsEnumEntity(BaseEnumEntity): self.use_empty_value = self.schema_data.get( "use_empty_value", not self.multiselection ) - self.empty_label = ( - self.schema_data.get("empty_label") or "< without host >" - ) + custom_labels = self.schema_data.get("custom_labels") or {} + + host_names = [ + "aftereffects", + "blender", + "celaction", + "fusion", + "harmony", + "hiero", + "houdini", + "maya", + "nuke", + "photoshop", + "resolve", + "tvpaint", + "unreal" + ] + if self.use_empty_value: + host_names.insert(0, "") + # Add default label for empty value if not available + if "" not in custom_labels: + custom_labels[""] = "< without host >" # These are hardcoded there is not list of available host in OpenPype - self.enum_items = [ - {"aftereffects": "aftereffects"}, - {"blender": "blender"}, - {"celaction": "celaction"}, - {"fusion": "fusion"}, - {"harmony": "harmony"}, - {"hiero": "hiero"}, - {"houdini": "houdini"}, - {"maya": "maya"}, - {"nuke": "nuke"}, - {"photoshop": "photoshop"}, - {"resolve": "resolve"}, - {"tvpaint": "tvpaint"}, - {"unreal": "unreal"} - ] - - if self.use_empty_value: - self.enum_items.insert(0, {"": self.empty_label}) - + enum_items = [] valid_keys = set() - for item in self.enum_items or []: - valid_keys.add(tuple(item.keys())[0]) + for key in host_names: + label = custom_labels.get(key, key) + valid_keys.add(key) + enum_items.append({key, label}) + self.enum_items = enum_items self.valid_keys = valid_keys if self.multiselection: diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 0ad13bfe1a..bbd53fa46b 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -276,7 +276,7 @@ - enumeration of available hosts - multiselection can be allowed with setting key `"multiselection"` to `True` (Default: `False`) - it is possible to add empty value (represented with empty string) with setting `"use_empty_value"` to `True` (Default: `False`) -- to modify label of empty value set `"empty_label"` key with your label (Default: `< without host >`) +- it is possible to set `"custom_labels"` for host names where key `""` is empty value (Default: `{}`) ``` { "key": "host", @@ -284,7 +284,10 @@ "type": "hosts-enum", "multiselection": false, "use_empty_value": true, - "empty_label": "N/A" + "custom_labels": { + "": "N/A", + "nuke": "Nuke" + } } ``` From 9cbb10251947577e3e7b2c9b542b088c7bf2bc3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 14:11:57 +0200 Subject: [PATCH 143/359] fix dict/set type --- openpype/settings/entities/enum_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index d197683afe..63e0afeb47 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -153,7 +153,7 @@ class HostsEnumEntity(BaseEnumEntity): for key in host_names: label = custom_labels.get(key, key) valid_keys.add(key) - enum_items.append({key, label}) + enum_items.append({key: label}) self.enum_items = enum_items self.valid_keys = valid_keys From b5d7da72404aa7edde91d1527d9f4978ba3bab73 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 15:19:01 +0200 Subject: [PATCH 144/359] nuke: create render crop format to nuke.root.format if selected to none --- .../hosts/nuke/plugins/create/create_write_render.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 9ddf0e4a87..a1381122ee 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -100,6 +100,13 @@ class CreateWriteRender(plugin.PypeCreator): "/{subset}.{frame}.{ext}")}) # add crop node to cut off all outside of format bounding box + # get width and height + try: + width, height = (selected_node.width(), selected_node.height()) + except AttributeError: + actual_format = nuke.root().knob('format').value() + width, height = (actual_format.width(), actual_format.height()) + _prenodes = [ { "name": "Crop01", @@ -108,8 +115,8 @@ class CreateWriteRender(plugin.PypeCreator): ("box", [ 0.0, 0.0, - selected_node.width(), - selected_node.height() + width, + height ]) ], "dependent": None From 7ca8ce40a2aecafa272406d2fbd1908dd12ab8a1 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 15:20:07 +0200 Subject: [PATCH 145/359] pep8 fix --- openpype/hosts/nuke/plugins/create/create_write_prerender.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index bb01236801..1b925014ad 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -128,5 +128,4 @@ class CreateWritePrerender(plugin.PypeCreator): w_node["first"].setValue(nuke.root()["first_frame"].value()) w_node["last"].setValue(nuke.root()["last_frame"].value()) - return write_node From e430797b39cd4acd061af10210bc89785922477c Mon Sep 17 00:00:00 2001 From: jezscha Date: Tue, 22 Jun 2021 13:36:12 +0000 Subject: [PATCH 146/359] Create draft PR for #1688 From 4f66746d9e1e44ed69d95a9f49dfb995084c5102 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:46:40 +0200 Subject: [PATCH 147/359] collect scene bg color --- .../plugins/publish/collect_workfile_data.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 13c6c9eb78..d8bb03f541 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -1,5 +1,6 @@ import os import json +import tempfile import pyblish.api import avalon.api @@ -153,9 +154,45 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): "sceneMarkIn": int(mark_in_frame), "sceneMarkInState": mark_in_state == "set", "sceneMarkOut": int(mark_out_frame), - "sceneMarkOutState": mark_out_state == "set" + "sceneMarkOutState": mark_out_state == "set", + "sceneBgColor": self._get_bg_color() } self.log.debug( "Scene data: {}".format(json.dumps(scene_data, indent=4)) ) context.data.update(scene_data) + + def _get_bg_color(self): + """Background color set on scene. + + Is important for review exporting where scene bg color is used as + background. + """ + output_file = tempfile.NamedTemporaryFile( + mode="w", prefix="a_tvp_", suffix=".txt", delete=False + ) + output_file.close() + output_filepath = output_file.name.replace("\\", "/") + george_script_lines = [ + # Variable containing full path to output file + "output_path = \"{}\"".format(output_filepath), + "tv_background", + "bg_color = result", + # Write data to output file + ( + "tv_writetextfile" + " \"strict\" \"append\" '\"'output_path'\"' bg_color" + ) + ] + + george_script = "\n".join(george_script_lines) + lib.execute_george_through_file(george_script) + + with open(output_filepath, "r") as stream: + data = stream.read() + + os.remove(output_filepath) + data = data.strip() + if not data: + return None + return data.split(" ") From ddd3668277ace0136e95e3b489e46e935bfeb2ad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:51:56 +0200 Subject: [PATCH 148/359] renamed thumbnail_bg_color to review_bg_color --- .../plugins/publish/extract_sequence.py | 32 ++++++------------- .../defaults/project_settings/tvpaint.json | 2 +- .../schema_project_tvpaint.json | 8 +++-- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index c13066d2c3..6e5ec39f46 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -14,7 +14,7 @@ class ExtractSequence(pyblish.api.Extractor): families = ["review", "renderPass", "renderLayer"] # Modifiable with settings - thumbnail_bg = [255, 255, 255, 255] + review_bg = [255, 255, 255, 255] def process(self, instance): self.log.info( @@ -299,20 +299,6 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure source_img = Image.open(first_frame_filepath) - if source_img.mode.lower() == "rgba": - bg_color = self._get_thumbnail_bg_color() - self.log.debug("Adding thumbnail background color {}.".format( - " ".join(bg_color) - )) - bg_image = Image.new("RGBA", source_img.size, bg_color) - thumbnail_obj = Image.alpha_composite(bg_image, source_img) - thumbnail_obj.convert("RGB").save(thumbnail_filepath) - else: - self.log.info(( - "Source for thumbnail has mode \"{}\" (Expected: RGBA)." - " Can't use thubmanail background color." - ).format(source_img.mode)) - source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath @@ -411,9 +397,9 @@ class ExtractSequence(pyblish.api.Extractor): # Composite background only on rgba images # - just making sure if source_img.mode.lower() == "rgba": - bg_color = self._get_thumbnail_bg_color() + bg_color = self._get_review_bg_color() self.log.debug("Adding thumbnail background color {}.".format( - " ".join(bg_color) + " ".join([str(val) for val in bg_color]) )) bg_image = Image.new("RGBA", source_img.size, bg_color) thumbnail_obj = Image.alpha_composite(bg_image, source_img) @@ -428,13 +414,13 @@ class ExtractSequence(pyblish.api.Extractor): return output_filenames, thumbnail_filepath - def _get_thumbnail_bg_color(self): + def _get_review_bg_color(self): red = green = blue = 255 - if self.thumbnail_bg: - if len(self.thumbnail_bg) == 4: - red, green, blue, _ = self.thumbnail_bg - elif len(self.thumbnail_bg) == 3: - red, green, blue = self.thumbnail_bg + if self.review_bg: + if len(self.review_bg) == 4: + red, green, blue, _ = self.review_bg + elif len(self.review_bg) == 3: + red, green, blue = self.review_bg return (red, green, blue) def _render_layer( diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 25b7056ce1..763802a73f 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -1,7 +1,7 @@ { "publish": { "ExtractSequence": { - "thumbnail_bg": [ + "review_bg": [ 255, 255, 255, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 1894384bb9..67aa4b0a06 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -18,10 +18,14 @@ "label": "ExtractSequence", "is_group": true, "children": [ + { + "type": "label", + "label": "Review BG color is used for whole scene review and for thumbnails." + }, { "type": "color", - "key": "thumbnail_bg", - "label": "Thumbnail BG color", + "key": "review_bg", + "label": "Review BG color", "use_alpha": false } ] From d5b0976b2d07ef75f2c4c3f58897094b6918f904 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:52:26 +0200 Subject: [PATCH 149/359] pass scene bg color to review extaction --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 6e5ec39f46..dc6e7eb64f 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -56,6 +56,8 @@ class ExtractSequence(pyblish.api.Extractor): handle_start = instance.context.data["handleStart"] handle_end = instance.context.data["handleEnd"] + scene_bg_color = instance.context.data["sceneBgColor"] + # --- Fallbacks ---------------------------------------------------- # This is required if validations of ranges are ignored. # - all of this code won't change processing if range to render @@ -123,7 +125,8 @@ class ExtractSequence(pyblish.api.Extractor): if instance.data["family"] == "review": output_filenames, thumbnail_fullpath = self.render_review( - filename_template, output_dir, mark_in, mark_out + filename_template, output_dir, mark_in, mark_out, + scene_bg_color ) else: # Render output @@ -244,7 +247,9 @@ class ExtractSequence(pyblish.api.Extractor): for path in repre_filepaths ] - def render_review(self, filename_template, output_dir, mark_in, mark_out): + def render_review( + self, filename_template, output_dir, mark_in, mark_out, scene_bg_color + ): """ Export images from TVPaint using `tv_savesequence` command. Args: @@ -255,6 +260,8 @@ class ExtractSequence(pyblish.api.Extractor): output_dir (str): Directory where files will be stored. mark_in (int): Starting frame index from which export will begin. mark_out (int): On which frame index export will end. + scene_bg_color (list): Bg color set in scene. Result of george + script command `tv_background`. Retruns: tuple: With 2 items first is list of filenames second is path to From 70bd4460a054c8843145874aed9e5fa9168399b6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:52:46 +0200 Subject: [PATCH 150/359] change bg color to color from settings and back --- .../tvpaint/plugins/publish/extract_sequence.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index dc6e7eb64f..c09cf08d82 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -1,5 +1,6 @@ import os import shutil +import copy import tempfile import pyblish.api @@ -273,7 +274,11 @@ class ExtractSequence(pyblish.api.Extractor): filename_template.format(frame=mark_in) ) + bg_color = self._get_review_bg_color() + george_script_lines = [ + # Change bg color to color from settings + "tv_background \"color\" {} {} {}".format(*bg_color), "tv_SaveMode \"PNG\"", "export_path = \"{}\"".format( first_frame_filepath.replace("\\", "/") @@ -282,6 +287,18 @@ class ExtractSequence(pyblish.api.Extractor): mark_in, mark_out ) ] + if scene_bg_color: + # Change bg color back to previous scene bg color + _scene_bg_color = copy.deepcopy(scene_bg_color) + bg_type = _scene_bg_color.pop(0) + orig_color_command = [ + "tv_background", + "\"{}\"".format(bg_type) + ] + orig_color_command.extend(_scene_bg_color) + + george_script_lines.append(" ".join(orig_color_command)) + lib.execute_george_through_file("\n".join(george_script_lines)) first_frame_filepath = None From e708f95f0dd61f069f3add62ebb6e7fec64ab77a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 22 Jun 2021 15:53:16 +0200 Subject: [PATCH 151/359] thumbnail creation from review is simplified as bg color is already applied on output --- .../hosts/tvpaint/plugins/publish/extract_sequence.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index c09cf08d82..536df2adb0 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -318,11 +318,13 @@ class ExtractSequence(pyblish.api.Extractor): if first_frame_filepath is None: first_frame_filepath = filepath - thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") + thumbnail_filepath = None if first_frame_filepath and os.path.exists(first_frame_filepath): - # Composite background only on rgba images - # - just making sure + thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg") source_img = Image.open(first_frame_filepath) + if source_img.mode.lower() != "rgb": + source_img = source_img.convert("RGB") + source_img.save(thumbnail_filepath) return output_filenames, thumbnail_filepath From 9105b9eb6d314471f4a0ba91994494e964de55f3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:16:13 +0200 Subject: [PATCH 152/359] hiero: fixing creator (deepcopy) --- .../hiero/plugins/create/create_shot_clip.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/hiero/plugins/create/create_shot_clip.py b/openpype/hosts/hiero/plugins/create/create_shot_clip.py index 25be9f090b..0c5bf93a3f 100644 --- a/openpype/hosts/hiero/plugins/create/create_shot_clip.py +++ b/openpype/hosts/hiero/plugins/create/create_shot_clip.py @@ -1,3 +1,4 @@ +from copy import deepcopy import openpype.hosts.hiero.api as phiero # from openpype.hosts.hiero.api import plugin, lib # reload(lib) @@ -206,20 +207,24 @@ class CreateShotClip(phiero.Creator): presets = None def process(self): + # Creator copy of object attributes that are modified during `process` + presets = deepcopy(self.presets) + gui_inputs = deepcopy(self.gui_inputs) + # get key pares from presets and match it on ui inputs - for k, v in self.gui_inputs.items(): + for k, v in gui_inputs.items(): if v["type"] in ("dict", "section"): # nested dictionary (only one level allowed # for sections and dict) for _k, _v in v["value"].items(): - if self.presets.get(_k): - self.gui_inputs[k][ - "value"][_k]["value"] = self.presets[_k] - if self.presets.get(k): - self.gui_inputs[k]["value"] = self.presets[k] + if presets.get(_k): + gui_inputs[k][ + "value"][_k]["value"] = presets[_k] + if presets.get(k): + gui_inputs[k]["value"] = presets[k] # open widget for plugins inputs - widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs) + widget = self.widget(self.gui_name, self.gui_info, gui_inputs) widget.exec_() if len(self.selected) < 1: From 0c91aee6e16500e620f2135eb54ad18c551df6da Mon Sep 17 00:00:00 2001 From: jezscha Date: Tue, 22 Jun 2021 14:19:38 +0000 Subject: [PATCH 153/359] Create draft PR for #1740 From 39076fac360e4906173384cbb884e63a70465a05 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Jun 2021 16:48:43 +0200 Subject: [PATCH 154/359] use pathlib, executable fixes --- openpype/hosts/unreal/api/lib.py | 173 +++++++++--------- .../unreal/hooks/pre_workfile_preparation.py | 29 ++- 2 files changed, 103 insertions(+), 99 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 56f92088b3..4760d1a78f 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -7,6 +7,7 @@ import json from distutils import dir_util import subprocess import re +from pathlib import Path from collections import OrderedDict from openpype.api import get_project_settings @@ -147,8 +148,8 @@ def _parse_launcher_locations(install_json_path: str) -> dict: def create_unreal_project(project_name: str, ue_version: str, - pr_dir: str, - engine_path: str, + pr_dir: Path, + engine_path: Path, dev_mode: bool = False, env: dict = None) -> None: """This will create `.uproject` file at specified location. @@ -162,8 +163,8 @@ def create_unreal_project(project_name: str, Args: project_name (str): Name of the project. ue_version (str): Unreal engine version (like 4.23). - pr_dir (str): Path to directory where project will be created. - engine_path (str): Path to Unreal Engine installation. + pr_dir (Path): Path to directory where project will be created. + engine_path (Path): Path to Unreal Engine installation. dev_mode (bool, optional): Flag to trigger C++ style Unreal project needing Visual Studio and other tools to compile plugins from sources. This will trigger automatically if `Binaries` @@ -192,19 +193,20 @@ def create_unreal_project(project_name: str, # created in different UE4 version. When user convert such project # to his UE4 version, Engine ID is replaced in uproject file. If some # other user tries to open it, it will present him with similar error. + ue4_modules = Path() if platform.system().lower() == "windows": - ue4_modules = os.path.join(engine_path, "Engine", "Binaries", - "Win64", "UE4Editor.modules") + ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + "Win64", "UE4Editor.modules")) if platform.system().lower() == "linux": - ue4_modules = os.path.join(engine_path, "Engine", "Binaries", - "Linux", "UE4Editor.modules") + ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + "Linux", "UE4Editor.modules")) if platform.system().lower() == "darwin": - ue4_modules = os.path.join(engine_path, "Engine", "Binaries", - "Mac", "UE4Editor.modules") + ue4_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + "Mac", "UE4Editor.modules")) - if os.path.exists(ue4_modules): + if ue4_modules.exists(): print("--- Loading Engine ID from modules file ...") with open(ue4_modules, "r") as mp: loaded_modules = json.load(mp) @@ -217,16 +219,16 @@ def create_unreal_project(project_name: str, if os.path.isdir(env.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project - plugins_path = os.path.join(pr_dir, "Plugins") - avalon_plugin_path = os.path.join(plugins_path, "Avalon") - if not os.path.isdir(avalon_plugin_path): - os.makedirs(avalon_plugin_path, exist_ok=True) + plugins_path = pr_dir / "Plugins" + avalon_plugin_path = plugins_path / "Avalon" + if not avalon_plugin_path.is_dir(): + avalon_plugin_path.mkdir(parents=True, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree(os.environ.get("AVALON_UNREAL_PLUGIN"), - avalon_plugin_path) + avalon_plugin_path.as_posix()) - if (not os.path.isdir(os.path.join(avalon_plugin_path, "Binaries")) - or not os.path.join(avalon_plugin_path, "Intermediate")): + if not (avalon_plugin_path / "Binaries").is_dir() \ + or not (avalon_plugin_path / "Intermediate").is_dir(): dev_mode = True # data for project file @@ -247,14 +249,14 @@ def create_unreal_project(project_name: str, # support offline installation. # Otherwise clone UnrealEnginePython to Plugins directory # https://github.com/20tab/UnrealEnginePython.git - uep_path = os.path.join(plugins_path, "UnrealEnginePython") + uep_path = plugins_path / "UnrealEnginePython" if env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): os.makedirs(uep_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree( env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), - uep_path) + uep_path.as_posix()) else: # WARNING: this will trigger dev_mode, because we need to compile # this plugin. @@ -262,13 +264,13 @@ def create_unreal_project(project_name: str, import git git.Repo.clone_from( "https://github.com/20tab/UnrealEnginePython.git", - uep_path) + uep_path.as_posix()) data["Plugins"].append( {"Name": "UnrealEnginePython", "Enabled": True}) - if (not os.path.isdir(os.path.join(uep_path, "Binaries")) - or not os.path.join(uep_path, "Intermediate")): + if not (uep_path / "Binaries").is_dir() \ + or not (uep_path / "Intermediate").is_dir(): dev_mode = True if dev_mode or preset["dev_mode"]: @@ -287,10 +289,8 @@ def create_unreal_project(project_name: str, # now we need to fix python path in: # `UnrealEnginePython.Build.cs` # to point to our python - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="r") as f: + with open(uep_path / "Source" / "UnrealEnginePython" / + "UnrealEnginePython.Build.cs", mode="r") as f: build_file = f.read() fix = build_file.replace( @@ -298,14 +298,12 @@ def create_unreal_project(project_name: str, 'private string pythonHome = "{}";'.format( sys.base_prefix.replace("\\", "/"))) - with open(os.path.join( - uep_path, "Source", - "UnrealEnginePython", - "UnrealEnginePython.Build.cs"), mode="w") as f: + with open(uep_path / "Source" / "UnrealEnginePython" / + "UnrealEnginePython.Build.cs", mode="w") as f: f.write(fix) # write project file - project_file = os.path.join(pr_dir, "{}.uproject".format(project_name)) + project_file = pr_dir / f"{project_name}.uproject" with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) @@ -316,44 +314,43 @@ def create_unreal_project(project_name: str, if int(ue_version.split(".")[0]) == 4 and \ int(ue_version.split(".")[1]) < 25: if platform.system().lower() == "windows": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python", "Win64", - "python.exe") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python/Win64/python.exe") if platform.system().lower() == "linux": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python", "Linux", - "bin", "python") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python/Linux/bin/python") if platform.system().lower() == "darwin": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python", "Mac", - "bin", "python") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python/Mac/bin/python") - if python_path: - subprocess.run([python_path, "-m", + if python_path.exists(): + subprocess.run([python_path.as_posix(), "-m", "pip", "install", "pyside"]) else: raise NotImplementedError("Unsupported platform") else: # install PySide2 inside newer engines if platform.system().lower() == "windows": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python3", "Win64", - "python3.exe") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Win64/pythonw.exe") if platform.system().lower() == "linux": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python3", "Linux", - "bin", "python3") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Linux" / + "bin" / "python3") if platform.system().lower() == "darwin": - python_path = os.path.join(engine_path, "Engine", "Binaries", - "ThirdParty", "Python3", "Mac", - "bin", "python3") + python_path = (engine_path / "Engine" / "Binaries" / + "ThirdParty" / "Python3" / "Mac" / + "bin" / "python3") if python_path: - subprocess.run([python_path, "-m", + if not python_path.exists(): + raise RuntimeError( + f"Unreal Python not found at {python_path}") + subprocess.run([python_path.as_posix(), "-m", "pip", "install", "pyside2"]) else: raise NotImplementedError("Unsupported platform") @@ -362,7 +359,7 @@ def create_unreal_project(project_name: str, _prepare_cpp_project(project_file, engine_path) -def _prepare_cpp_project(project_file: str, engine_path: str) -> None: +def _prepare_cpp_project(project_file: Path, engine_path: Path) -> None: """Prepare CPP Unreal Project. This function will add source files needed for project to be @@ -379,13 +376,13 @@ def _prepare_cpp_project(project_file: str, engine_path: str) -> None: engine_path (str): Path to unreal engine associated with project. """ - project_name = os.path.splitext(os.path.basename(project_file))[0] - project_dir = os.path.dirname(project_file) - targets_dir = os.path.join(project_dir, "Source") - sources_dir = os.path.join(targets_dir, project_name) + project_name = project_file.stem + project_dir = project_file.parent + targets_dir = project_dir / "Source" + sources_dir = targets_dir / project_name - os.makedirs(sources_dir, exist_ok=True) - os.makedirs(os.path.join(project_dir, "Content"), exist_ok=True) + sources_dir.mkdir(parents=True, exist_ok=True) + (project_dir / "Content").mkdir(parents=True, exist_ok=True) module_target = ''' using UnrealBuildTool; @@ -460,63 +457,59 @@ class {1}_API A{0}GameModeBase : public AGameModeBase }}; '''.format(project_name, project_name.upper()) - with open(os.path.join( - targets_dir, f"{project_name}.Target.cs"), mode="w") as f: + with open(targets_dir / f"{project_name}.Target.cs", mode="w") as f: f.write(module_target) - with open(os.path.join( - targets_dir, f"{project_name}Editor.Target.cs"), mode="w") as f: + with open(targets_dir / f"{project_name}Editor.Target.cs", mode="w") as f: f.write(editor_module_target) - with open(os.path.join( - sources_dir, f"{project_name}.Build.cs"), mode="w") as f: + with open(sources_dir / f"{project_name}.Build.cs", mode="w") as f: f.write(module_build) - with open(os.path.join( - sources_dir, f"{project_name}.cpp"), mode="w") as f: + with open(sources_dir / f"{project_name}.cpp", mode="w") as f: f.write(module_cpp) - with open(os.path.join( - sources_dir, f"{project_name}.h"), mode="w") as f: + with open(sources_dir / f"{project_name}.h", mode="w") as f: f.write(module_header) - with open(os.path.join( - sources_dir, f"{project_name}GameModeBase.cpp"), mode="w") as f: + with open(sources_dir / f"{project_name}GameModeBase.cpp", mode="w") as f: f.write(game_mode_cpp) - with open(os.path.join( - sources_dir, f"{project_name}GameModeBase.h"), mode="w") as f: + with open(sources_dir / f"{project_name}GameModeBase.h", mode="w") as f: f.write(game_mode_h) - u_build_tool = (f"{engine_path}/Engine/Binaries/DotNET/" - "UnrealBuildTool.exe") + u_build_tool = Path( + engine_path / "Engine/Binaries/DotNET/UnrealBuildTool.exe") u_header_tool = None + arch = "Win64" if platform.system().lower() == "windows": - u_header_tool = (f"{engine_path}/Engine/Binaries/Win64/" - f"UnrealHeaderTool.exe") + arch = "Win64" + u_header_tool = Path( + engine_path / "Engine/Binaries/Win64/UnrealHeaderTool.exe") elif platform.system().lower() == "linux": - u_header_tool = (f"{engine_path}/Engine/Binaries/Linux/" - f"UnrealHeaderTool") + arch = "Linux" + u_header_tool = Path( + engine_path / "Engine/Binaries/Linux/UnrealHeaderTool") elif platform.system().lower() == "darwin": # we need to test this out - u_header_tool = (f"{engine_path}/Engine/Binaries/Mac/" - f"UnrealHeaderTool") + arch = "Mac" + u_header_tool = Path( + engine_path / "Engine/Binaries/Mac/UnrealHeaderTool") if not u_header_tool: raise NotImplementedError("Unsupported platform") - u_build_tool = u_build_tool.replace("\\", "/") - u_header_tool = u_header_tool.replace("\\", "/") - - command1 = [u_build_tool, "-projectfiles", f"-project={project_file}", - "-progress"] + command1 = [u_build_tool.as_posix(), "-projectfiles", + f"-project={project_file}", "-progress"] subprocess.run(command1) - command2 = [u_build_tool, f"-ModuleWithSuffix={project_name},3555" - "Win64", "Development", "-TargetType=Editor" - f'-Project="{project_file}"', f'"{project_file}"' + command2 = [u_build_tool.as_posix(), + f"-ModuleWithSuffix={project_name},3555", arch, + "Development", "-TargetType=Editor", + f'-Project={project_file}', + f'{project_file}', "-IgnoreJunk"] subprocess.run(command2) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index c292730fb1..2c4dd822bc 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Hook to launch Unreal and prepare projects.""" import os +from pathlib import Path +import platform from openpype.lib import ( PreLaunchHook, @@ -50,7 +52,7 @@ class UnrealPrelaunchHook(PreLaunchHook): )) unreal_project_name = f"P{unreal_project_name}" - project_path = os.path.join(workdir, unreal_project_name) + project_path = Path(os.path.join(workdir, unreal_project_name)) self.log.info(( f"{self.signature} requested UE4 version: " @@ -73,13 +75,21 @@ class UnrealPrelaunchHook(PreLaunchHook): f"detected [ {engine_version} ]" )) - os.makedirs(project_path, exist_ok=True) + ue4_path = Path(detected[engine_version]) / "Engine/Binaries" + if platform.system().lower() == "windows": + ue4_path = ue4_path / "Win64/UE4Editor.exe" - project_file = os.path.join( - project_path, - f"{unreal_project_name}.uproject" - ) - if not os.path.isfile(project_file): + elif platform.system().lower() == "linux": + ue4_path = ue4_path / "Linux/UE4Editor" + + elif platform.system().lower() == "darwin": + ue4_path = ue4_path / "Mac/UE4Editor" + + self.launch_context.launch_args.append(ue4_path.as_posix()) + project_path.mkdir(parents=True, exist_ok=True) + + project_file = project_path / f"{unreal_project_name}.uproject" + if not project_file.is_file(): engine_path = detected[engine_version] self.log.info(( f"{self.signature} creating unreal " @@ -95,8 +105,9 @@ class UnrealPrelaunchHook(PreLaunchHook): unreal_project_name, engine_version, project_path, - engine_path=engine_path + engine_path=Path(engine_path) ) # Append project file to launch arguments - self.launch_context.launch_args.append(f"\"{project_file}\"") + self.launch_context.launch_args.append( + f"\"{project_file.as_posix()}\"") From 3a2f29150b7b7f110c82990e9aa3e37b6793cc21 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 22 Jun 2021 16:53:15 +0200 Subject: [PATCH 155/359] fix indents --- openpype/hosts/unreal/api/lib.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 4760d1a78f..8f05a63273 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -338,13 +338,11 @@ def create_unreal_project(project_name: str, if platform.system().lower() == "linux": python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Linux" / - "bin" / "python3") + "Python3/Linux/bin/python3") if platform.system().lower() == "darwin": - python_path = (engine_path / "Engine" / "Binaries" / - "ThirdParty" / "Python3" / "Mac" / - "bin" / "python3") + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Mac/bin/python3") if python_path: if not python_path.exists(): From c739967b48c78ab540df9bc283d370776b203c89 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:53:32 +0200 Subject: [PATCH 156/359] hiero: collector ignoring audio clips --- .../hiero/plugins/publish/precollect_instances.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 6e5c640e35..40c6be0f78 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -46,12 +46,6 @@ class PrecollectInstances(pyblish.api.ContextPlugin): source_clip = track_item.source() self.log.debug("clip_name: {}".format(clip_name)) - # get clips subtracks and anotations - annotations = self.clip_annotations(source_clip) - subtracks = self.clip_subtrack(track_item) - self.log.debug("Annotations: {}".format(annotations)) - self.log.debug(">> Subtracks: {}".format(subtracks)) - # get openpype tag data tag_data = phiero.get_track_item_pype_data(track_item) self.log.debug("__ tag_data: {}".format(pformat(tag_data))) @@ -62,6 +56,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if tag_data.get("id") != "pyblish.avalon.instance": continue + # get clips subtracks and anotations + annotations = self.clip_annotations(source_clip) + subtracks = self.clip_subtrack(track_item) + self.log.debug("Annotations: {}".format(annotations)) + self.log.debug(">> Subtracks: {}".format(subtracks)) + # solve handles length tag_data["handleStart"] = min( tag_data["handleStart"], int(track_item.handleInLength())) From 374f327ccd9145f23849c3627752db4b118c9101 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 22 Jun 2021 16:58:09 +0200 Subject: [PATCH 157/359] pep8 fix --- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 40c6be0f78..4984849aa7 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -56,7 +56,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if tag_data.get("id") != "pyblish.avalon.instance": continue - # get clips subtracks and anotations + # get clips subtracks and anotations annotations = self.clip_annotations(source_clip) subtracks = self.clip_subtrack(track_item) self.log.debug("Annotations: {}".format(annotations)) From ac6278f076128946f7f4fce6563a6485c59a571f Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 23 Jun 2021 03:40:30 +0000 Subject: [PATCH 158/359] [Automated] Bump version --- CHANGELOG.md | 9 +++++++-- openpype/version.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c0a25c7d..9fe6de4bfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [3.2.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...HEAD) **🚀 Enhancements** +- Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) - Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) - Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) @@ -14,10 +15,14 @@ **🐛 Bug fixes** +- Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) +- Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734) +- Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733) - TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) - Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) - Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) - Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) +- Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) ## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-18) @@ -25,6 +30,7 @@ **🐛 Bug fixes** +- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Remove publish highlight icon in AE [\#1664](https://github.com/pypeclub/OpenPype/pull/1664) **Merged pull requests:** @@ -74,7 +80,6 @@ **🐛 Bug fixes** -- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) - Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) diff --git a/openpype/version.py b/openpype/version.py index ece0359506..f527bd4d6e 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.1" +__version__ = "3.2.0-nightly.2" From 43ddbf89991b49a31463bbd3c190fbd8e19be36d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 23 Jun 2021 11:01:11 +0200 Subject: [PATCH 159/359] update acre commit to fixed version of acre --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 48e6f95469..30dbe50c19 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ develop = false type = "git" url = "https://github.com/pypeclub/acre.git" reference = "master" -resolved_reference = "efc1b8faa8f84568538b936688ae6f7604dd194c" +resolved_reference = "68784b7eb5b7bb5f409b61ab31d4403878a3e1b7" [[package]] name = "aiohttp" From 946821b9fdf59c24a47ae627a5bb0d29ea82026b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 23 Jun 2021 13:16:58 +0200 Subject: [PATCH 160/359] Updated submodule repos/avalon-core --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index efde026e5a..d8be0bdb37 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit efde026e5aad72dac0e69848005419e2c4f067f2 +Subproject commit d8be0bdb37961e32243f1de0eb9696e86acf7443 From e6111daec83c49fecaebb125374dfd6a3cfb8d41 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:16:51 +0200 Subject: [PATCH 161/359] created SchemasHub for handling schemas --- openpype/settings/entities/lib.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 05f4ea64f8..9d13655a8f 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -426,3 +426,49 @@ class OverrideState: DEFAULTS = OverrideStateItem(0, "Defaults") STUDIO = OverrideStateItem(1, "Studio overrides") PROJECT = OverrideStateItem(2, "Project Overrides") + + +class SchemasHub: + def __init__(self, schema_subfolder): + from openpype.settings import entities + + # Define known abstract classes + known_abstract_classes = ( + entities.BaseEntity, + entities.BaseItemEntity, + entities.ItemEntity, + entities.EndpointEntity, + entities.InputEntity, + entities.BaseEnumEntity + ) + + self._loaded_types = {} + _gui_types = [] + for attr in dir(entities): + item = getattr(entities, attr) + # Filter classes + if not inspect.isclass(item): + continue + + # Skip classes that do not inherit from BaseEntity + if not issubclass(item, entities.BaseEntity): + continue + + # Skip class that is abstract by design + if item in known_abstract_classes: + continue + + if inspect.isabstract(item): + # Create an object to get crash and get traceback + item() + + # Backwards compatibility + # Single entity may have multiple schema types + for schema_type in item.schema_types: + self._loaded_types[schema_type] = item + + if item.gui_type: + _gui_types.append(item) + self._gui_types = tuple(_gui_types) + + self._schema_subfolder = schema_subfolder From 070ba3070af4a61d240aeb4bef166c425c79b947 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:20:07 +0200 Subject: [PATCH 162/359] added api callbacks to SchemaHub --- openpype/settings/entities/lib.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 9d13655a8f..879e3d9cad 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -472,3 +472,19 @@ class SchemasHub: self._gui_types = tuple(_gui_types) self._schema_subfolder = schema_subfolder + + @property + def gui_types(self): + return self._gui_types + + def get_schema(self, schema_name): + pass + + def get_template(self, template_name): + pass + + def resolve_schema_data(self, schema_data): + pass + + def create_schema_object(self, schema_data, *args, **kwargs): + pass From e3c4c91f3e91ab9480408d3b1f5f5445297b6a57 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:21:41 +0200 Subject: [PATCH 163/359] use schema hub inside root entity --- openpype/settings/entities/root_entities.py | 87 ++++++--------------- 1 file changed, 22 insertions(+), 65 deletions(-) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 401d3980c9..9bb32382fb 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -1,7 +1,6 @@ import os import json import copy -import inspect from abc import abstractmethod @@ -10,8 +9,7 @@ from .lib import ( NOT_SET, WRAPPER_TYPES, OverrideState, - get_studio_settings_schema, - get_project_settings_schema + SchemasHub ) from .exceptions import ( SchemaError, @@ -53,7 +51,12 @@ class RootEntity(BaseItemEntity): """ schema_types = ["root"] - def __init__(self, schema_data, reset): + def __init__(self, schema_hub, reset, main_schema_name=None): + self.schema_hub = schema_hub + if not main_schema_name: + main_schema_name = "schema_main" + schema_data = schema_hub.get_schema(main_schema_name) + super(RootEntity, self).__init__(schema_data) self._require_restart_callbacks = [] self._item_ids_require_restart = set() @@ -143,11 +146,13 @@ class RootEntity(BaseItemEntity): child_obj = self.create_schema_object(children_schema, self) self.children.append(child_obj) added_children.append(child_obj) - if isinstance(child_obj, self._gui_types): + if isinstance(child_obj, self.schema_hub.gui_types): continue if child_obj.key in self.non_gui_children: - raise KeyError("Duplicated key \"{}\"".format(child_obj.key)) + raise KeyError( + "Duplicated key \"{}\"".format(child_obj.key) + ) self.non_gui_children[child_obj.key] = child_obj if not first: @@ -160,9 +165,6 @@ class RootEntity(BaseItemEntity): # Store `self` to `root_item` for children entities self.root_item = self - self._loaded_types = None - self._gui_types = None - # Children are stored by key as keys are immutable and are defined by # schema self.valid_value_types = (dict, ) @@ -201,54 +203,9 @@ class RootEntity(BaseItemEntity): Available entities are loaded on first run. Children entities can call this method. """ - if self._loaded_types is None: - # Load available entities - from openpype.settings import entities - - # Define known abstract classes - known_abstract_classes = ( - entities.BaseEntity, - entities.BaseItemEntity, - entities.ItemEntity, - entities.EndpointEntity, - entities.InputEntity, - entities.BaseEnumEntity - ) - - self._loaded_types = {} - _gui_types = [] - for attr in dir(entities): - item = getattr(entities, attr) - # Filter classes - if not inspect.isclass(item): - continue - - # Skip classes that do not inherit from BaseEntity - if not issubclass(item, entities.BaseEntity): - continue - - # Skip class that is abstract by design - if item in known_abstract_classes: - continue - - if inspect.isabstract(item): - # Create an object to get crash and get traceback - item() - - # Backwards compatibility - # Single entity may have multiple schema types - for schema_type in item.schema_types: - self._loaded_types[schema_type] = item - - if item.gui_type: - _gui_types.append(item) - self._gui_types = tuple(_gui_types) - - klass = self._loaded_types.get(schema_data["type"]) - if not klass: - raise KeyError("Unknown type \"{}\"".format(schema_data["type"])) - - return klass(schema_data, *args, **kwargs) + return self.schema_hub.create_schema_object( + schema_data, *args, **kwargs + ) def set_override_state(self, state): """Set override state and trigger it on children. @@ -492,13 +449,13 @@ class SystemSettings(RootEntity): and debugging purposes. """ def __init__( - self, set_studio_state=True, reset=True, schema_data=None + self, set_studio_state=True, reset=True, schema_hub=None ): - if schema_data is None: + if schema_hub is None: # Load system schemas - schema_data = get_studio_settings_schema() + schema_hub = SchemasHub("system_schema") - super(SystemSettings, self).__init__(schema_data, reset) + super(SystemSettings, self).__init__(schema_hub, reset) if set_studio_state: self.set_studio_state() @@ -605,17 +562,17 @@ class ProjectSettings(RootEntity): project_name=None, change_state=True, reset=True, - schema_data=None + schema_hub=None ): self._project_name = project_name self._system_settings_entity = None - if schema_data is None: + if schema_hub is None: # Load system schemas - schema_data = get_project_settings_schema() + schema_hub = SchemasHub("projects_schema") - super(ProjectSettings, self).__init__(schema_data, reset) + super(ProjectSettings, self).__init__(schema_hub, reset) if change_state: if self.project_name is None: From cb7e0c957ca94fa89162a242c7b64ab77df6e25b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:22:19 +0200 Subject: [PATCH 164/359] use resolving where templates can be used --- .../settings/entities/dict_immutable_keys_entity.py | 12 +++++++++++- openpype/settings/entities/root_entities.py | 13 ++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 052bbda4d0..c965dc3b5a 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -1,4 +1,5 @@ import copy +import collections from .lib import ( WRAPPER_TYPES, @@ -138,7 +139,16 @@ class DictImmutableKeysEntity(ItemEntity): method when handling gui wrappers. """ added_children = [] - for children_schema in schema_data["children"]: + children_deque = collections.deque() + for _children_schema in schema_data["children"]: + children_schemas = self.schema_hub.resolve_schema_data( + _children_schema + ) + for children_schema in children_schemas: + children_deque.append(children_schema) + + while children_deque: + children_schema = children_deque.popleft() if children_schema["type"] in WRAPPER_TYPES: _children_schema = copy.deepcopy(children_schema) wrapper_children = self._add_children( diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 9bb32382fb..1833535a07 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -1,6 +1,7 @@ import os import json import copy +import collections from abc import abstractmethod @@ -133,7 +134,17 @@ class RootEntity(BaseItemEntity): def _add_children(self, schema_data, first=True): added_children = [] - for children_schema in schema_data["children"]: + children_deque = collections.deque() + for _children_schema in schema_data["children"]: + children_schemas = self.schema_hub.resolve_schema_data( + _children_schema + ) + for children_schema in children_schemas: + children_deque.append(children_schema) + + while children_deque: + children_schema = children_deque.popleft() + if children_schema["type"] in WRAPPER_TYPES: _children_schema = copy.deepcopy(children_schema) wrapper_children = self._add_children( From d02e6eda745b35b6bf7c221b369d157a8b901bea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:22:39 +0200 Subject: [PATCH 165/359] gave access to event hub for all entities --- openpype/settings/entities/base_entity.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index c6bff1ff47..0e29a35e1f 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -885,7 +885,11 @@ class ItemEntity(BaseItemEntity): def create_schema_object(self, *args, **kwargs): """Reference method for creation of entities defined in RootEntity.""" - return self.root_item.create_schema_object(*args, **kwargs) + return self.schema_hub.create_schema_object(*args, **kwargs) + + @property + def schema_hub(self): + return self.root_item.schema_hub def get_entity_from_path(self, path): return self.root_item.get_entity_from_path(path) From 0fc16b25767e8f7d614553def050b49552ba09a0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:23:49 +0200 Subject: [PATCH 166/359] moved template filling functions under SchemaHub --- openpype/settings/entities/lib.py | 351 +++++++++++++++--------------- 1 file changed, 175 insertions(+), 176 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 879e3d9cad..74de3e6ffa 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -25,182 +25,6 @@ TEMPLATE_METADATA_KEYS = ( template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") -def _pop_metadata_item(template): - found_idx = None - for idx, item in enumerate(template): - if not isinstance(item, dict): - continue - - for key in TEMPLATE_METADATA_KEYS: - if key in item: - found_idx = idx - break - - if found_idx is not None: - break - - metadata_item = {} - if found_idx is not None: - metadata_item = template.pop(found_idx) - return metadata_item - - -def _fill_schema_template_data( - template, template_data, skip_paths, required_keys=None, missing_keys=None -): - first = False - if required_keys is None: - first = True - - if "skip_paths" in template_data: - skip_paths = template_data["skip_paths"] - if not isinstance(skip_paths, list): - skip_paths = [skip_paths] - - # Cleanup skip paths (skip empty values) - skip_paths = [path for path in skip_paths if path] - - required_keys = set() - missing_keys = set() - - # Copy template data as content may change - template = copy.deepcopy(template) - - # Get metadata item from template - metadata_item = _pop_metadata_item(template) - - # Check for default values for template data - default_values = metadata_item.get(DEFAULT_VALUES_KEY) or {} - - for key, value in default_values.items(): - if key not in template_data: - template_data[key] = value - - if not template: - output = template - - elif isinstance(template, list): - # Store paths by first part if path - # - None value says that whole key should be skipped - skip_paths_by_first_key = {} - for path in skip_paths: - parts = path.split("/") - key = parts.pop(0) - if key not in skip_paths_by_first_key: - skip_paths_by_first_key[key] = [] - - value = "/".join(parts) - skip_paths_by_first_key[key].append(value or None) - - output = [] - for item in template: - # Get skip paths for children item - _skip_paths = [] - if not isinstance(item, dict): - pass - - elif item.get("type") in WRAPPER_TYPES: - _skip_paths = copy.deepcopy(skip_paths) - - elif skip_paths_by_first_key: - # Check if this item should be skipped - key = item.get("key") - if key and key in skip_paths_by_first_key: - _skip_paths = skip_paths_by_first_key[key] - # Skip whole item if None is in skip paths value - if None in _skip_paths: - continue - - output_item = _fill_schema_template_data( - item, template_data, _skip_paths, required_keys, missing_keys - ) - if output_item: - output.append(output_item) - - elif isinstance(template, dict): - output = {} - for key, value in template.items(): - output[key] = _fill_schema_template_data( - value, template_data, skip_paths, required_keys, missing_keys - ) - if output.get("type") in WRAPPER_TYPES and not output.get("children"): - return {} - - elif isinstance(template, STRING_TYPE): - # TODO find much better way how to handle filling template data - template = template.replace("{{", "__dbcb__").replace("}}", "__decb__") - for replacement_string in template_key_pattern.findall(template): - key = str(replacement_string[1:-1]) - required_keys.add(key) - if key not in template_data: - missing_keys.add(key) - continue - - value = template_data[key] - if replacement_string == template: - # Replace the value with value from templates data - # - with this is possible to set value with different type - template = value - else: - # Only replace the key in string - template = template.replace(replacement_string, value) - - output = template.replace("__dbcb__", "{").replace("__decb__", "}") - - else: - output = template - - if first and missing_keys: - raise SchemaTemplateMissingKeys(missing_keys, required_keys) - - return output - - -def _fill_schema_template(child_data, schema_collection, schema_templates): - template_name = child_data["name"] - template = schema_templates.get(template_name) - if template is None: - if template_name in schema_collection: - raise KeyError(( - "Schema \"{}\" is used as `schema_template`" - ).format(template_name)) - raise KeyError("Schema template \"{}\" was not found".format( - template_name - )) - - # Default value must be dictionary (NOT list) - # - empty list would not add any item if `template_data` are not filled - template_data = child_data.get("template_data") or {} - if isinstance(template_data, dict): - template_data = [template_data] - - skip_paths = child_data.get("skip_paths") or [] - if isinstance(skip_paths, STRING_TYPE): - skip_paths = [skip_paths] - - output = [] - for single_template_data in template_data: - try: - filled_child = _fill_schema_template_data( - template, single_template_data, skip_paths - ) - - except SchemaTemplateMissingKeys as exc: - raise SchemaTemplateMissingKeys( - exc.missing_keys, exc.required_keys, template_name - ) - - for item in filled_child: - filled_item = _fill_inner_schemas( - item, schema_collection, schema_templates - ) - if filled_item["type"] == "schema_template": - output.extend(_fill_schema_template( - filled_item, schema_collection, schema_templates - )) - else: - output.append(filled_item) - return output def _fill_inner_schemas(schema_data, schema_collection, schema_templates): @@ -488,3 +312,178 @@ class SchemasHub: def create_schema_object(self, schema_data, *args, **kwargs): pass + + def _fill_schema_template(self, child_data, template_def): + template_name = child_data["name"] + + # Default value must be dictionary (NOT list) + # - empty list would not add any item if `template_data` are not filled + template_data = child_data.get("template_data") or {} + if isinstance(template_data, dict): + template_data = [template_data] + + skip_paths = child_data.get("skip_paths") or [] + if isinstance(skip_paths, STRING_TYPE): + skip_paths = [skip_paths] + + output = [] + for single_template_data in template_data: + try: + output.extend(self._fill_schema_template_data( + template_def, single_template_data, skip_paths + )) + + except SchemaTemplateMissingKeys as exc: + raise SchemaTemplateMissingKeys( + exc.missing_keys, exc.required_keys, template_name + ) + return output + + def _fill_schema_template_data( + self, + template, + template_data, + skip_paths, + required_keys=None, + missing_keys=None + ): + first = False + if required_keys is None: + first = True + + if "skip_paths" in template_data: + skip_paths = template_data["skip_paths"] + if not isinstance(skip_paths, list): + skip_paths = [skip_paths] + + # Cleanup skip paths (skip empty values) + skip_paths = [path for path in skip_paths if path] + + required_keys = set() + missing_keys = set() + + # Copy template data as content may change + template = copy.deepcopy(template) + + # Get metadata item from template + metadata_item = self._pop_metadata_item(template) + + # Check for default values for template data + default_values = metadata_item.get(DEFAULT_VALUES_KEY) or {} + + for key, value in default_values.items(): + if key not in template_data: + template_data[key] = value + + if not template: + output = template + + elif isinstance(template, list): + # Store paths by first part if path + # - None value says that whole key should be skipped + skip_paths_by_first_key = {} + for path in skip_paths: + parts = path.split("/") + key = parts.pop(0) + if key not in skip_paths_by_first_key: + skip_paths_by_first_key[key] = [] + + value = "/".join(parts) + skip_paths_by_first_key[key].append(value or None) + + output = [] + for item in template: + # Get skip paths for children item + _skip_paths = [] + if not isinstance(item, dict): + pass + + elif item.get("type") in WRAPPER_TYPES: + _skip_paths = copy.deepcopy(skip_paths) + + elif skip_paths_by_first_key: + # Check if this item should be skipped + key = item.get("key") + if key and key in skip_paths_by_first_key: + _skip_paths = skip_paths_by_first_key[key] + # Skip whole item if None is in skip paths value + if None in _skip_paths: + continue + + output_item = self._fill_schema_template_data( + item, + template_data, + _skip_paths, + required_keys, + missing_keys + ) + if output_item: + output.append(output_item) + + elif isinstance(template, dict): + output = {} + for key, value in template.items(): + output[key] = self._fill_schema_template_data( + value, + template_data, + skip_paths, + required_keys, + missing_keys + ) + if ( + output.get("type") in WRAPPER_TYPES + and not output.get("children") + ): + return {} + + elif isinstance(template, STRING_TYPE): + # TODO find much better way how to handle filling template data + template = ( + template + .replace("{{", "__dbcb__") + .replace("}}", "__decb__") + ) + for replacement_string in template_key_pattern.findall(template): + key = str(replacement_string[1:-1]) + required_keys.add(key) + if key not in template_data: + missing_keys.add(key) + continue + + value = template_data[key] + if replacement_string == template: + # Replace the value with value from templates data + # - with this is possible to set value with different type + template = value + else: + # Only replace the key in string + template = template.replace(replacement_string, value) + + output = template.replace("__dbcb__", "{").replace("__decb__", "}") + + else: + output = template + + if first and missing_keys: + raise SchemaTemplateMissingKeys(missing_keys, required_keys) + + return output + + def _pop_metadata_item(self, template_def): + found_idx = None + for idx, item in enumerate(template_def): + if not isinstance(item, dict): + continue + + for key in TEMPLATE_METADATA_KEYS: + if key in item: + found_idx = idx + break + + if found_idx is not None: + break + + metadata_item = {} + if found_idx is not None: + metadata_item = template_def.pop(found_idx) + return metadata_item From d0b32e129271806132d84cb37011dc69ba68ad76 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:24:29 +0200 Subject: [PATCH 167/359] implemented loading of schemas for schema hub --- openpype/settings/entities/lib.py | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 74de3e6ffa..aae98067f7 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -296,6 +296,11 @@ class SchemasHub: self._gui_types = tuple(_gui_types) self._schema_subfolder = schema_subfolder + self._crashed_on_load = {} + loaded_templates, loaded_schemas = self._load_schemas() + + self._loaded_templates = loaded_templates + self._loaded_schemas = loaded_schemas @property def gui_types(self): @@ -313,6 +318,65 @@ class SchemasHub: def create_schema_object(self, schema_data, *args, **kwargs): pass + def _load_schemas(self): + dirpath = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "schemas", + self._schema_subfolder + ) + loaded_schemas = {} + loaded_templates = {} + for root, _, filenames in os.walk(dirpath): + for filename in filenames: + basename, ext = os.path.splitext(filename) + if ext != ".json": + continue + + filepath = os.path.join(root, filename) + with open(filepath, "r") as json_stream: + try: + schema_data = json.load(json_stream) + except Exception as exc: + msg = str(exc) + print("Unable to parse JSON file {}\n{}".format( + filepath, msg + )) + self._crashed_on_load[basename] = { + "filepath": filepath, + "message": msg + } + continue + + if basename in self._crashed_on_load: + crashed_item = self._crashed_on_load[basename] + raise KeyError(( + "Duplicated filename \"{}\"." + " One of them crashed on load \"{}\" {}" + ).format( + filename, + crashed_item["filpath"], + crashed_item["message"] + )) + + if isinstance(schema_data, list): + if basename in loaded_templates: + raise KeyError( + "Duplicated template filename \"{}\"".format( + filename + ) + ) + loaded_templates[basename] = schema_data + else: + if basename in loaded_schemas: + raise KeyError( + "Duplicated schema filename \"{}\"".format( + filename + ) + ) + loaded_schemas[basename] = schema_data + + return loaded_templates, loaded_schemas + def _fill_schema_template(self, child_data, template_def): template_name = child_data["name"] From 770e33d0f9a2e507f3499ead0b655a014ae4748b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:24:54 +0200 Subject: [PATCH 168/359] implemented get_template and get_schema --- openpype/settings/entities/lib.py | 38 +++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index aae98067f7..cdc154e441 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -307,10 +307,44 @@ class SchemasHub: return self._gui_types def get_schema(self, schema_name): - pass + if schema_name not in self._loaded_schemas: + if schema_name in self._loaded_templates: + raise KeyError(( + "Template \"{}\" is used as `schema`" + ).format(schema_name)) + + elif schema_name in self._crashed_on_load: + crashed_item = self._crashed_on_load[schema_name] + raise KeyError( + "Unable to parse schema file \"{}\". {}".format( + crashed_item["filpath"], crashed_item["message"] + ) + ) + + raise KeyError( + "Schema \"{}\" was not found".format(schema_name) + ) + return copy.deepcopy(self._loaded_schemas[schema_name]) def get_template(self, template_name): - pass + if template_name not in self._loaded_templates: + if template_name in self._loaded_schemas: + raise KeyError(( + "Schema \"{}\" is used as `template`" + ).format(template_name)) + + elif template_name in self._crashed_on_load: + crashed_item = self._crashed_on_load[template_name] + raise KeyError( + "Unable to parse templace file \"{}\". {}".format( + crashed_item["filpath"], crashed_item["message"] + ) + ) + + raise KeyError( + "Template \"{}\" was not found".format(template_name) + ) + return copy.deepcopy(self._loaded_templates[template_name]) def resolve_schema_data(self, schema_data): pass From cae6f7e6209b879908ead4d31b75ab99e818b774 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:25:42 +0200 Subject: [PATCH 169/359] implemented create_schema_object which handle creation of entities by schema data --- openpype/settings/entities/lib.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index cdc154e441..0e67e6500a 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -350,7 +350,17 @@ class SchemasHub: pass def create_schema_object(self, schema_data, *args, **kwargs): - pass + schema_type = schema_data["type"] + if schema_type in ("schema", "template", "schema_template"): + raise ValueError( + "Got unresolved schema data of type \"{}\"".format(schema_type) + ) + + klass = self._loaded_types.get(schema_type) + if not klass: + raise KeyError("Unknown type \"{}\"".format(schema_type)) + + return klass(schema_data, *args, **kwargs) def _load_schemas(self): dirpath = os.path.join( From 9ec64866a35473126a82e69bd7fde11758079e51 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:26:07 +0200 Subject: [PATCH 170/359] implemented resolving for schemas and template items --- openpype/settings/entities/lib.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 0e67e6500a..a57a391c3a 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -347,7 +347,22 @@ class SchemasHub: return copy.deepcopy(self._loaded_templates[template_name]) def resolve_schema_data(self, schema_data): - pass + schema_type = schema_data["type"] + if schema_type not in ("schema", "template", "schema_template"): + return [schema_data] + + if schema_type == "schema": + return self.resolve_schema_data( + self.get_schema(schema_data["name"]) + ) + + template_name = schema_data["name"] + template_def = self.get_template(template_name) + + filled_template = self._fill_schema_template( + schema_data, template_def + ) + return filled_template def create_schema_object(self, schema_data, *args, **kwargs): schema_type = schema_data["type"] From d4d1e177ae49e7bdbdc27821dd68d5619e64ee85 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:27:15 +0200 Subject: [PATCH 171/359] removed unused functions --- openpype/settings/entities/lib.py | 73 ------------------------------- 1 file changed, 73 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index a57a391c3a..933905a3b2 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -25,71 +25,6 @@ TEMPLATE_METADATA_KEYS = ( template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") - - -def _fill_inner_schemas(schema_data, schema_collection, schema_templates): - if schema_data["type"] == "schema": - raise ValueError("First item in schema data can't be schema.") - - children_key = "children" - object_type_key = "object_type" - for item_key in (children_key, object_type_key): - children = schema_data.get(item_key) - if not children: - continue - - if object_type_key == item_key: - if not isinstance(children, dict): - continue - children = [children] - - new_children = [] - for child in children: - child_type = child["type"] - if child_type == "schema": - schema_name = child["name"] - if schema_name not in schema_collection: - if schema_name in schema_templates: - raise KeyError(( - "Schema template \"{}\" is used as `schema`" - ).format(schema_name)) - raise KeyError( - "Schema \"{}\" was not found".format(schema_name) - ) - - filled_child = _fill_inner_schemas( - schema_collection[schema_name], - schema_collection, - schema_templates - ) - - elif child_type in ("template", "schema_template"): - for filled_child in _fill_schema_template( - child, schema_collection, schema_templates - ): - new_children.append(filled_child) - continue - - else: - filled_child = _fill_inner_schemas( - child, schema_collection, schema_templates - ) - - new_children.append(filled_child) - - if item_key == object_type_key: - if len(new_children) != 1: - raise KeyError(( - "Failed to fill object type with type: {} | name {}" - ).format( - child_type, str(child.get("name")) - )) - new_children = new_children[0] - - schema_data[item_key] = new_children - return schema_data - - # TODO reimplement logic inside entities def validate_environment_groups_uniquenes( schema_data, env_groups=None, keys=None @@ -170,14 +105,6 @@ def get_gui_schema(subfolder, main_schema_name): return main_schema -def get_studio_settings_schema(): - return get_gui_schema("system_schema", "schema_main") - - -def get_project_settings_schema(): - return get_gui_schema("projects_schema", "schema_main") - - class OverrideStateItem: """Object used as item for `OverrideState` enum. From 4bc9aa821fb5e2b47a34513458c0ae957844305d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:41:53 +0200 Subject: [PATCH 172/359] add missing import --- openpype/settings/entities/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 933905a3b2..437fa05aca 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -2,6 +2,7 @@ import os import re import json import copy +import inspect from .exceptions import ( SchemaTemplateMissingKeys, From 8f00b0eb2ff88f4826c37be6513af7dc2485139f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 10:53:11 +0200 Subject: [PATCH 173/359] few smaller organization changes --- openpype/settings/entities/lib.py | 108 ++++++++++++++++++------------ 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 437fa05aca..6e1231e2f6 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -181,54 +181,26 @@ class OverrideState: class SchemasHub: - def __init__(self, schema_subfolder): - from openpype.settings import entities - - # Define known abstract classes - known_abstract_classes = ( - entities.BaseEntity, - entities.BaseItemEntity, - entities.ItemEntity, - entities.EndpointEntity, - entities.InputEntity, - entities.BaseEnumEntity - ) + def __init__(self, schema_subfolder, reset=True): + self._schema_subfolder = schema_subfolder self._loaded_types = {} - _gui_types = [] - for attr in dir(entities): - item = getattr(entities, attr) - # Filter classes - if not inspect.isclass(item): - continue + self._gui_types = tuple() - # Skip classes that do not inherit from BaseEntity - if not issubclass(item, entities.BaseEntity): - continue - - # Skip class that is abstract by design - if item in known_abstract_classes: - continue - - if inspect.isabstract(item): - # Create an object to get crash and get traceback - item() - - # Backwards compatibility - # Single entity may have multiple schema types - for schema_type in item.schema_types: - self._loaded_types[schema_type] = item - - if item.gui_type: - _gui_types.append(item) - self._gui_types = tuple(_gui_types) - - self._schema_subfolder = schema_subfolder self._crashed_on_load = {} - loaded_templates, loaded_schemas = self._load_schemas() + self._loaded_templates = {} + self._loaded_schemas = {} - self._loaded_templates = loaded_templates - self._loaded_schemas = loaded_schemas + # It doesn't make sence to reload types on each reset as they can't be + # changed + self._load_types() + + # Trigger reset + if reset: + self.reset() + + def reset(self): + self._load_schemas() @property def gui_types(self): @@ -305,7 +277,54 @@ class SchemasHub: return klass(schema_data, *args, **kwargs) + def _load_types(self): + from openpype.settings import entities + + # Define known abstract classes + known_abstract_classes = ( + entities.BaseEntity, + entities.BaseItemEntity, + entities.ItemEntity, + entities.EndpointEntity, + entities.InputEntity, + entities.BaseEnumEntity + ) + + self._loaded_types = {} + _gui_types = [] + for attr in dir(entities): + item = getattr(entities, attr) + # Filter classes + if not inspect.isclass(item): + continue + + # Skip classes that do not inherit from BaseEntity + if not issubclass(item, entities.BaseEntity): + continue + + # Skip class that is abstract by design + if item in known_abstract_classes: + continue + + if inspect.isabstract(item): + # Create an object to get crash and get traceback + item() + + # Backwards compatibility + # Single entity may have multiple schema types + for schema_type in item.schema_types: + self._loaded_types[schema_type] = item + + if item.gui_type: + _gui_types.append(item) + self._gui_types = tuple(_gui_types) + def _load_schemas(self): + # Refresh all affecting variables + self._crashed_on_load = {} + self._loaded_templates = {} + self._loaded_schemas = {} + dirpath = os.path.join( os.path.dirname(os.path.abspath(__file__)), "schemas", @@ -362,7 +381,8 @@ class SchemasHub: ) loaded_schemas[basename] = schema_data - return loaded_templates, loaded_schemas + self._loaded_templates = loaded_templates + self._loaded_schemas = loaded_schemas def _fill_schema_template(self, child_data, template_def): template_name = child_data["name"] From 568c6e5f61e9a912048f6e192a0f3b0d9b022d6e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 11:19:02 +0200 Subject: [PATCH 174/359] use shorter method names --- openpype/settings/entities/lib.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 6e1231e2f6..2d38468877 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -259,7 +259,7 @@ class SchemasHub: template_name = schema_data["name"] template_def = self.get_template(template_name) - filled_template = self._fill_schema_template( + filled_template = self._fill_template( schema_data, template_def ) return filled_template @@ -384,7 +384,7 @@ class SchemasHub: self._loaded_templates = loaded_templates self._loaded_schemas = loaded_schemas - def _fill_schema_template(self, child_data, template_def): + def _fill_template(self, child_data, template_def): template_name = child_data["name"] # Default value must be dictionary (NOT list) @@ -400,7 +400,7 @@ class SchemasHub: output = [] for single_template_data in template_data: try: - output.extend(self._fill_schema_template_data( + output.extend(self._fill_template_data( template_def, single_template_data, skip_paths )) @@ -410,7 +410,7 @@ class SchemasHub: ) return output - def _fill_schema_template_data( + def _fill_template_data( self, template, template_data, @@ -481,7 +481,7 @@ class SchemasHub: if None in _skip_paths: continue - output_item = self._fill_schema_template_data( + output_item = self._fill_template_data( item, template_data, _skip_paths, @@ -494,7 +494,7 @@ class SchemasHub: elif isinstance(template, dict): output = {} for key, value in template.items(): - output[key] = self._fill_schema_template_data( + output[key] = self._fill_template_data( value, template_data, skip_paths, From 083dd58b3937edfb6c6903aacbd7ed82ea298c90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 11:20:07 +0200 Subject: [PATCH 175/359] handle wrapper types in create object --- openpype/settings/entities/lib.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 2d38468877..e6b73b7066 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -271,6 +271,12 @@ class SchemasHub: "Got unresolved schema data of type \"{}\"".format(schema_type) ) + if schema_type in WRAPPER_TYPES: + raise ValueError(( + "Function `create_schema_object` can't create entities" + " of any wrapper type. Got type: \"{}\"" + ).format(schema_type)) + klass = self._loaded_types.get(schema_type) if not klass: raise KeyError("Unknown type \"{}\"".format(schema_type)) From 9cfd8af2bf341dfe8a603dac84c21690d793c2b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 11:20:15 +0200 Subject: [PATCH 176/359] added brief docstrings --- openpype/settings/entities/lib.py | 143 +++++++++++++----------------- 1 file changed, 63 insertions(+), 80 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index e6b73b7066..31071a2d30 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -26,86 +26,6 @@ TEMPLATE_METADATA_KEYS = ( template_key_pattern = re.compile(r"(\{.*?[^{0]*\})") -# TODO reimplement logic inside entities -def validate_environment_groups_uniquenes( - schema_data, env_groups=None, keys=None -): - is_first = False - if env_groups is None: - is_first = True - env_groups = {} - keys = [] - - my_keys = copy.deepcopy(keys) - key = schema_data.get("key") - if key: - my_keys.append(key) - - env_group_key = schema_data.get("env_group_key") - if env_group_key: - if env_group_key not in env_groups: - env_groups[env_group_key] = [] - env_groups[env_group_key].append("/".join(my_keys)) - - children = schema_data.get("children") - if not children: - return - - for child in children: - validate_environment_groups_uniquenes( - child, env_groups, copy.deepcopy(my_keys) - ) - - if is_first: - invalid = {} - for env_group_key, key_paths in env_groups.items(): - if len(key_paths) > 1: - invalid[env_group_key] = key_paths - - if invalid: - raise SchemaDuplicatedEnvGroupKeys(invalid) - - -def validate_schema(schema_data): - validate_environment_groups_uniquenes(schema_data) - - -def get_gui_schema(subfolder, main_schema_name): - dirpath = os.path.join( - os.path.dirname(__file__), - "schemas", - subfolder - ) - loaded_schemas = {} - loaded_schema_templates = {} - for root, _, filenames in os.walk(dirpath): - for filename in filenames: - basename, ext = os.path.splitext(filename) - if ext != ".json": - continue - - filepath = os.path.join(root, filename) - with open(filepath, "r") as json_stream: - try: - schema_data = json.load(json_stream) - except Exception as exc: - raise ValueError(( - "Unable to parse JSON file {}\n{}" - ).format(filepath, str(exc))) - if isinstance(schema_data, list): - loaded_schema_templates[basename] = schema_data - else: - loaded_schemas[basename] = schema_data - - main_schema = _fill_inner_schemas( - loaded_schemas[main_schema_name], - loaded_schemas, - loaded_schema_templates - ) - validate_schema(main_schema) - return main_schema - - class OverrideStateItem: """Object used as item for `OverrideState` enum. @@ -207,6 +127,7 @@ class SchemasHub: return self._gui_types def get_schema(self, schema_name): + """Get schema definition data by it's name.""" if schema_name not in self._loaded_schemas: if schema_name in self._loaded_templates: raise KeyError(( @@ -227,6 +148,7 @@ class SchemasHub: return copy.deepcopy(self._loaded_schemas[schema_name]) def get_template(self, template_name): + """Get template definition data by it's name.""" if template_name not in self._loaded_templates: if template_name in self._loaded_schemas: raise KeyError(( @@ -247,6 +169,19 @@ class SchemasHub: return copy.deepcopy(self._loaded_templates[template_name]) def resolve_schema_data(self, schema_data): + """Resolve single item schema data as few types can be expanded. + + This is mainly for 'schema' and 'template' types. Type 'schema' does + not have entity representation and 'template' may contain more than one + output schemas. + + In other cases is retuned passed schema item in list. + + Goal is to have schema and template resolving at one place. + + Returns: + list: Resolved schema data. + """ schema_type = schema_data["type"] if schema_type not in ("schema", "template", "schema_template"): return [schema_data] @@ -265,6 +200,19 @@ class SchemasHub: return filled_template def create_schema_object(self, schema_data, *args, **kwargs): + """Create entity for passed schema data. + + Args: + schema_data(dict): Schema definition of settings entity. + + Returns: + ItemEntity: Created entity for passed schema data item. + + Raises: + ValueError: When 'schema', 'template' or any of wrapper types are + passed. + KeyError: When type of passed schema is not known. + """ schema_type = schema_data["type"] if schema_type in ("schema", "template", "schema_template"): raise ValueError( @@ -284,6 +232,8 @@ class SchemasHub: return klass(schema_data, *args, **kwargs) def _load_types(self): + """Prepare entity types for cretion of their objects.""" + from openpype.settings import entities # Define known abstract classes @@ -326,6 +276,8 @@ class SchemasHub: self._gui_types = tuple(_gui_types) def _load_schemas(self): + """Load schema definitions from json files.""" + # Refresh all affecting variables self._crashed_on_load = {} self._loaded_templates = {} @@ -391,6 +343,30 @@ class SchemasHub: self._loaded_schemas = loaded_schemas def _fill_template(self, child_data, template_def): + """Fill template based on schema definition and template definition. + + Based on `child_data` is `template_def` modified and result is + returned. + + Template definition may have defined data to fill which + should be filled with data from child data. + + Child data may contain more than one output definition of an template. + + Child data can define paths to skip. Path is full path of an item + which won't be returned. + + TODO: + Be able to handle wrapper items here. + + Args: + child_data(dict): Schema data of template item. + template_def(dict): Template definition that will be filled with + child_data. + + Returns: + list: Resolved template always returns list of schemas. + """ template_name = child_data["name"] # Default value must be dictionary (NOT list) @@ -424,6 +400,7 @@ class SchemasHub: required_keys=None, missing_keys=None ): + """Fill template values with data from schema data.""" first = False if required_keys is None: first = True @@ -547,6 +524,12 @@ class SchemasHub: return output def _pop_metadata_item(self, template_def): + """Pop template metadata from template definition. + + Template metadata may define default values if are not passed from + schema data. + """ + found_idx = None for idx, item in enumerate(template_def): if not isinstance(item, dict): From 5d651bbc618537c5750d5c55d179b6282fb84249 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 12:52:36 +0200 Subject: [PATCH 177/359] don't add ftrack family in tvpaint collect instances --- openpype/hosts/tvpaint/plugins/publish/collect_instances.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py index 4468bfae40..e496b144cd 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py @@ -103,8 +103,6 @@ class CollectInstances(pyblish.api.ContextPlugin): instance.data["layers"] = copy.deepcopy( context.data["layersData"] ) - # Add ftrack family - instance.data["families"].append("ftrack") elif family == "renderLayer": instance = self.create_render_layer_instance( @@ -186,9 +184,6 @@ class CollectInstances(pyblish.api.ContextPlugin): instance_data["layers"] = group_layers - # Add ftrack family - instance_data["families"].append("ftrack") - return context.create_instance(**instance_data) def create_render_pass_instance(self, context, instance_data): From 43dca9e537eb8d9ca10c2a3dcd7f981a4165dc8f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 12:52:54 +0200 Subject: [PATCH 178/359] add tvpaint family definition in ftrack collect ftrack family --- .../defaults/project_settings/ftrack.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 03ecf024a6..88f4e1e2e7 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -259,6 +259,26 @@ "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] + }, + { + "hosts": [ + "tvpaint" + ], + "families": [ + "renderPass" + ], + "tasks": [], + "add_ftrack_family": false, + "advanced_filtering": [] + }, + { + "hosts": [ + "tvpaint" + ], + "families": [], + "tasks": [], + "add_ftrack_family": true, + "advanced_filtering": [] } ] }, From c916d128bc05dcd2f99e01a1015901a875947226 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 24 Jun 2021 13:12:36 +0200 Subject: [PATCH 179/359] mark support for Unreal Python Engine deprecated --- openpype/hosts/unreal/api/lib.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 8f05a63273..6231fd6f33 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -245,17 +245,21 @@ def create_unreal_project(project_name: str, } if preset["install_unreal_python_engine"]: - # If `PYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from there to - # support offline installation. + # WARNING: This is deprecated as Unreal Engine Python project + # is on hold and is mainly replaced in 4.26 by Epics own + # Python implementation. + # --------------------------------------------------------------- + # If `OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from + # there to support offline installation. # Otherwise clone UnrealEnginePython to Plugins directory # https://github.com/20tab/UnrealEnginePython.git uep_path = plugins_path / "UnrealEnginePython" - if env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): + if env.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): os.makedirs(uep_path, exist_ok=True) dir_util._path_created = {} dir_util.copy_tree( - env.get("PYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), + env.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), uep_path.as_posix()) else: # WARNING: this will trigger dev_mode, because we need to compile From 34ca28d856b71dbda31d59b2ff772d5bccd8ce66 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Thu, 24 Jun 2021 13:03:07 +0000 Subject: [PATCH 180/359] [Automated] Bump version --- CHANGELOG.md | 41 ++++++++++++++++++++++++++++------------- openpype/version.py | 2 +- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe6de4bfa..3fe2ce33cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,44 +1,59 @@ # Changelog -## [3.2.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) +- PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) - Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) - Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) +- Toggle Ftrack upload in StandalonePublisher [\#1708](https://github.com/pypeclub/OpenPype/pull/1708) - Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) - Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699) - Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695) **🐛 Bug fixes** +- Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745) +- hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743) +- Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) +- Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741) - Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) +- TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735) - Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734) - Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733) +- Ftrack sync status [\#1732](https://github.com/pypeclub/OpenPype/pull/1732) - TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) - Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) - Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) - Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) +- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) -## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-18) +## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.2...2.18.3) - -**🐛 Bug fixes** - -- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) -- Remove publish highlight icon in AE [\#1664](https://github.com/pypeclub/OpenPype/pull/1664) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4) + +**Merged pull requests:** + +- celaction fixes [\#1754](https://github.com/pypeclub/OpenPype/pull/1754) +- celaciton: audio subset changed data structure [\#1750](https://github.com/pypeclub/OpenPype/pull/1750) + +## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) + +**🐛 Bug fixes** + +- Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) **Merged pull requests:** -- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) - global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) -- \#683 - Validate frame range in Standalone Publisher [\#1680](https://github.com/pypeclub/OpenPype/pull/1680) -- Maya: Split model content validator [\#1654](https://github.com/pypeclub/OpenPype/pull/1654) ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) @@ -80,9 +95,9 @@ **🐛 Bug fixes** +- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) -- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) diff --git a/openpype/version.py b/openpype/version.py index f527bd4d6e..ce6cfec003 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.2" +__version__ = "3.2.0-nightly.3" From 78a92588bb91a4a70bca912c526a15d04870f211 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Jun 2021 16:06:42 +0200 Subject: [PATCH 181/359] deadline: nuke adding settings attributes - allowed environment keys - search replace in environment values --- .../plugins/publish/submit_nuke_deadline.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 7faa3393e5..879c92490b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -32,6 +32,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): department = "" limit_groups = {} use_gpu = False + env_allowed_keys = [] + env_search_replace_values = {} def process(self, instance): instance.data["toBeRenderedOn"] = "deadline" @@ -242,18 +244,18 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "PYBLISHPLUGINPATH", "NUKE_PATH", "TOOL_ENV", - "OPENPYPE_DEV", "FOUNDRY_LICENSE" ] + # add allowed keys from preset if any + if self.env_allowed_keys: + keys += self.env_allowed_keys + environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) # self.log.debug("enviro: {}".format(pprint(environment))) + for path in os.environ: - if path.lower().startswith('pype_'): - environment[path] = os.environ[path] - if path.lower().startswith('nuke_'): - environment[path] = os.environ[path] - if 'license' in path.lower(): + if path.lower().startswith('openpype_'): environment[path] = os.environ[path] clean_environment = {} @@ -285,6 +287,13 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): environment = clean_environment # to recognize job from PYPE for turning Event On/Off environment["OPENPYPE_RENDER_JOB"] = "1" + + # finally search replace in values of any key + if self.env_search_replace_values: + for key, value in environment.items(): + for _k, _v in self.env_search_replace_values.items(): + environment[key] = value.replace(_k, _v) + payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, From 248912f9fa3b0d3611ccf898343e45e43faf4da3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Jun 2021 16:15:23 +0200 Subject: [PATCH 182/359] settings: deadline nuke submission arguments --- .../defaults/project_settings/deadline.json | 2 ++ .../projects_schema/schema_project_deadline.json | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 2cc345d5ad..5861015f2c 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -29,6 +29,8 @@ "group": "", "department": "", "use_gpu": true, + "env_allowed_keys": [], + "env_search_replace_values": {}, "limit_groups": {} }, "HarmonySubmitDeadline": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index f6a8127951..3281c9ce4d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -173,6 +173,20 @@ "key": "use_gpu", "label": "Use GPU" }, + { + "type": "list", + "key": "env_allowed_keys", + "object_type": "text", + "label": "Allowed environment keys" + }, + { + "type": "dict-modifiable", + "key": "env_search_replace_values", + "label": "Search & replace in environment values", + "object_type": { + "type": "text" + } + }, { "type": "dict-modifiable", "key": "limit_groups", From a31524b75ced90dfc1f34f9295764347b828d557 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 16:28:08 +0200 Subject: [PATCH 183/359] try to format executable path with environments --- openpype/lib/applications.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index bed57d7022..a7dcb6dd55 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -460,6 +460,12 @@ class ApplicationExecutable: if os.path.exists(_executable): executable = _executable + # Try to format executable with environments + try: + executable = executable.format(**os.environ) + except Exception: + pass + self.executable_path = executable def __str__(self): From 29932c4766dc6711f09f947df42bb1a3703f401a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 16:54:24 +0200 Subject: [PATCH 184/359] pop others before expected keys are processed --- openpype/lib/anatomy.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py index c16c6e2e99..7a4a55363c 100644 --- a/openpype/lib/anatomy.py +++ b/openpype/lib/anatomy.py @@ -733,6 +733,9 @@ class Templates: continue default_key_values[key] = templates.pop(key) + # Pop "others" key before before expected keys are processed + other_templates = templates.pop("others") or {} + keys_by_subkey = {} for sub_key, sub_value in templates.items(): key_values = {} @@ -740,7 +743,6 @@ class Templates: key_values.update(sub_value) keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values) - other_templates = templates.get("others") or {} for sub_key, sub_value in other_templates.items(): if sub_key in keys_by_subkey: log.warning(( From c74216f082e2b2edd44839daf484dc7e82db73e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 19:05:50 +0200 Subject: [PATCH 185/359] fix quotes in path for extract thumbnail in standalone publisher --- .../standalonepublisher/plugins/publish/extract_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py index 963d47956a..0792254716 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py @@ -66,7 +66,6 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin): else: # Convert to jpeg if not yet full_input_path = os.path.join(thumbnail_repre["stagingDir"], file) - full_input_path = '"{}"'.format(full_input_path) self.log.info("input {}".format(full_input_path)) full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1] From 57bd695974b66f93406926000882d75f2d29794c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:05:49 +0200 Subject: [PATCH 186/359] added process_attribute_changes where previous logic happened --- .../event_push_frame_values_to_task.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index c0b3137455..613566f25d 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -131,6 +131,18 @@ class PushFrameValuesToTaskEvent(BaseEvent): name_low = object_type["name"].lower() object_types_by_name[name_low] = object_type + if interesting_data: + self.process_attribute_changes( + session, object_types_by_name, + interesting_data, changed_keys_by_object_id, + interest_entity_types, interest_attributes + ) + + def process_attribute_changes( + self, session, object_types_by_name, + interesting_data, changed_keys_by_object_id, + interest_entity_types, interest_attributes + ): # Prepare task object id task_object_id = object_types_by_name["task"]["id"] @@ -216,13 +228,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): task_entity_ids.add(task_id) parent_id_by_task_id[task_id] = task_entity["parent_id"] - self.finalize( + self.finalize_attribute_changes( session, interesting_data, changed_keys, attrs_by_obj_id, hier_attrs, task_entity_ids, parent_id_by_task_id ) - def finalize( + def finalize_attribute_changes( self, session, interesting_data, changed_keys, attrs_by_obj_id, hier_attrs, task_entity_ids, parent_id_by_task_id From 143d1205eedbe5266f02d6ad6a9fecb227919dce Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:06:08 +0200 Subject: [PATCH 187/359] collect also task changes if parent_id has changed --- .../event_push_frame_values_to_task.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 613566f25d..8c45efa91b 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -55,10 +55,6 @@ class PushFrameValuesToTaskEvent(BaseEvent): if entity_info.get("entityType") != "task": continue - # Skip `Task` entity type - if entity_info["entity_type"].lower() == "task": - continue - # Care only about changes of status changes = entity_info.get("changes") if not changes: @@ -74,6 +70,14 @@ class PushFrameValuesToTaskEvent(BaseEvent): if project_id is None: continue + # Skip `Task` entity type if parent didn't change + if entity_info["entity_type"].lower() == "task": + if ( + "parent_id" not in changes + or changes["parent_id"]["new"] is None + ): + continue + if project_id not in entities_info_by_project_id: entities_info_by_project_id[project_id] = [] entities_info_by_project_id[project_id].append(entity_info) From a4e84611febe1192d98a21b022a2090764864fad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:06:30 +0200 Subject: [PATCH 188/359] separate task parent changes and value changes --- .../event_push_frame_values_to_task.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 8c45efa91b..f0675bdbc8 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -121,11 +121,21 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return + # Separate value changes and task parent changes + _entities_info = [] + task_parent_changes = [] + for entity_info in entities_info: + if entity_info["entity_type"].lower() == "task": + task_parent_changes.append(entity_info) + else: + _entities_info.append(entity_info) + entities_info = _entities_info + # Filter entities info with changes interesting_data, changed_keys_by_object_id = self.filter_changes( session, event, entities_info, interest_attributes ) - if not interesting_data: + if not interesting_data and not task_parent_changes: return # Prepare object types From 18297588a59349ed41305c9741a7dcde868d5d97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:07:15 +0200 Subject: [PATCH 189/359] convert attributes and types to set --- .../event_handlers_server/event_push_frame_values_to_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index f0675bdbc8..443fdafd71 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -121,6 +121,9 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return + interest_attributes = set(self.interest_attributes) + interest_entity_types = set(self.interest_entity_types) + # Separate value changes and task parent changes _entities_info = [] task_parent_changes = [] From 70b91afe199f38652078c1081b36657e9ea8dcba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:15:45 +0200 Subject: [PATCH 190/359] implemented query_custom_attributes for querying custom attribute values from ftrack database --- openpype/modules/ftrack/lib/__init__.py | 4 +- .../modules/ftrack/lib/custom_attributes.py | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py index ce6d5284b6..9dc2d67279 100644 --- a/openpype/modules/ftrack/lib/__init__.py +++ b/openpype/modules/ftrack/lib/__init__.py @@ -13,7 +13,8 @@ from .custom_attributes import ( default_custom_attributes_definition, app_definitions_from_app_manager, tool_definitions_from_app_manager, - get_openpype_attr + get_openpype_attr, + query_custom_attributes ) from . import avalon_sync @@ -37,6 +38,7 @@ __all__ = ( "app_definitions_from_app_manager", "tool_definitions_from_app_manager", "get_openpype_attr", + "query_custom_attributes", "avalon_sync", diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py index f6b82c90b1..53facd4ab2 100644 --- a/openpype/modules/ftrack/lib/custom_attributes.py +++ b/openpype/modules/ftrack/lib/custom_attributes.py @@ -81,3 +81,60 @@ def get_openpype_attr(session, split_hierarchical=True, query_keys=None): return custom_attributes, hier_custom_attributes return custom_attributes + + +def join_query_keys(keys): + """Helper to join keys to query.""" + return ",".join(["\"{}\"".format(key) for key in keys]) + + +def query_custom_attributes(session, conf_ids, entity_ids, table_name=None): + """Query custom attribute values from ftrack database. + + Using ftrack call method result may differ based on used table name and + version of ftrack server. + + Args: + session(ftrack_api.Session): Connected ftrack session. + conf_id(list, set, tuple): Configuration(attribute) ids which are + queried. + entity_ids(list, set, tuple): Entity ids for which are values queried. + table_name(str): Table nam from which values are queried. Not + recommended to change until you know what it means. + """ + output = [] + # Just skip + if not conf_ids or not entity_ids: + return output + + if table_name is None: + table_name = "ContextCustomAttributeValue" + + # Prepare values to query + attributes_joined = join_query_keys(conf_ids) + attributes_len = len(conf_ids) + + # Query values in chunks + chunk_size = int(5000 / attributes_len) + # Make sure entity_ids is `list` for chunk selection + entity_ids = list(entity_ids) + for idx in range(0, len(entity_ids), chunk_size): + entity_ids_joined = join_query_keys( + entity_ids[idx:idx + chunk_size] + ) + + call_expr = [{ + "action": "query", + "expression": ( + "select value, entity_id from {}" + " where entity_id in ({}) and configuration_id in ({})" + ).format(table_name, entity_ids_joined, attributes_joined) + }] + if hasattr(session, "call"): + [result] = session.call(call_expr) + else: + [result] = session._call(call_expr) + + for item in result["data"]: + output.append(item) + return output From 0894f272de642c76836e6c90bf01e21c97ceb05a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:28:30 +0200 Subject: [PATCH 191/359] implemented _commit_changes for easier access --- .../event_push_frame_values_to_task.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 443fdafd71..d654b26114 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -155,6 +155,61 @@ class PushFrameValuesToTaskEvent(BaseEvent): interest_entity_types, interest_attributes ) + def _commit_changes(self, session, changes): + uncommited_changes = False + for idx, item in enumerate(changes): + new_value = item["new_value"] + attr_id = item["attr_id"] + entity_id = item["entity_id"] + attr_key = item["attr_key"] + + entity_key = collections.OrderedDict() + entity_key["configuration_id"] = attr_id + entity_key["entity_id"] = entity_id + self._cached_changes.append({ + "attr_key": attr_key, + "entity_id": entity_id, + "value": new_value, + "time": datetime.datetime.now() + }) + if new_value is None: + op = ftrack_api.operation.DeleteEntityOperation( + "CustomAttributeValue", + entity_key + ) + else: + op = ftrack_api.operation.UpdateEntityOperation( + "ContextCustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + new_value + ) + + session.recorded_operations.push(op) + self.log.info(( + "Changing Custom Attribute \"{}\" to value" + " \"{}\" on entity: {}" + ).format(attr_key, new_value, entity_id)) + + if (idx + 1) % 20 == 0: + uncommited_changes = False + try: + session.commit() + except Exception: + session.rollback() + self.log.warning( + "Changing of values failed.", exc_info=True + ) + else: + uncommited_changes = True + if uncommited_changes: + try: + session.commit() + except Exception: + session.rollback() + self.log.warning("Changing of values failed.", exc_info=True) + def process_attribute_changes( self, session, object_types_by_name, interesting_data, changed_keys_by_object_id, From 3251401da30a216887fa8fe351fb85fcf8d45d86 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:29:08 +0200 Subject: [PATCH 192/359] use _commit_changes in current implementation --- .../event_push_frame_values_to_task.py | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index d654b26114..c292d856da 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -332,6 +332,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): session, attr_ids, entity_ids, task_entity_ids, hier_attrs ) + changes = [] for entity_id, current_values in current_values_by_id.items(): parent_id = parent_id_by_task_id.get(entity_id) if not parent_id: @@ -356,39 +357,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): if new_value == old_value: continue - entity_key = collections.OrderedDict() - entity_key["configuration_id"] = attr_id - entity_key["entity_id"] = entity_id - self._cached_changes.append({ - "attr_key": attr_key, + changes.append({ + "new_value": new_value, + "attr_id": attr_id, "entity_id": entity_id, - "value": new_value, - "time": datetime.datetime.now() + "attr_key": attr_key }) - if new_value is None: - op = ftrack_api.operation.DeleteEntityOperation( - "CustomAttributeValue", - entity_key - ) - else: - op = ftrack_api.operation.UpdateEntityOperation( - "ContextCustomAttributeValue", - entity_key, - "value", - ftrack_api.symbol.NOT_SET, - new_value - ) - - session.recorded_operations.push(op) - self.log.info(( - "Changing Custom Attribute \"{}\" to value" - " \"{}\" on entity: {}" - ).format(attr_key, new_value, entity_id)) - try: - session.commit() - except Exception: - session.rollback() - self.log.warning("Changing of values failed.", exc_info=True) + self._commit_changes(session, changes) def filter_changes( self, session, event, entities_info, interest_attributes From 10f0604173d6bfeef2ced3375e97b951fc881fe5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:32:09 +0200 Subject: [PATCH 193/359] implemented task parent changes handling --- .../event_push_frame_values_to_task.py | 177 +++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index c292d856da..84f26dc57a 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -2,7 +2,10 @@ import collections import datetime import ftrack_api -from openpype.modules.ftrack.lib import BaseEvent +from openpype.modules.ftrack.lib import ( + BaseEvent, + query_custom_attributes +) class PushFrameValuesToTaskEvent(BaseEvent): @@ -155,6 +158,178 @@ class PushFrameValuesToTaskEvent(BaseEvent): interest_entity_types, interest_attributes ) + if task_parent_changes: + self.process_task_parent_change( + session, object_types_by_name, task_parent_changes, + interest_entity_types, interest_attributes + ) + + def process_task_parent_change( + self, session, object_types_by_name, task_parent_changes, + interest_entity_types, interest_attributes + ): + task_ids = set() + matching_parent_ids = set() + whole_hierarchy_ids = set() + parent_id_by_entity_id = {} + for entity_info in task_parent_changes: + parents = entity_info.get("parents") or [] + # Ignore entities with less parents than 2 + # NOTE entity itself is also part of "parents" value + if len(parents) < 2: + continue + + parent_info = parents[1] + if parent_info["entity_type"] not in interest_entity_types: + continue + + task_ids.add(entity_info["entityId"]) + matching_parent_ids.add(parent_info["entityId"]) + + prev_id = None + for item in parents: + item_id = item["entityId"] + whole_hierarchy_ids.add(item_id) + + if prev_id is None: + prev_id = item_id + continue + + parent_id_by_entity_id[prev_id] = item_id + if item["entityType"] == "show": + break + prev_id = item_id + + if not matching_parent_ids: + return + + entities = session.query( + "select object_type_id from TypedContext where id in ({})".format( + self.join_query_keys(matching_parent_ids) + ) + ) + object_type_ids = set() + for entity in entities: + object_type_ids.add(entity["object_type_id"]) + + # Prepare task object id + task_object_id = object_types_by_name["task"]["id"] + object_type_ids.add(task_object_id) + + attrs_by_obj_id, hier_attrs = self.attrs_configurations( + session, object_type_ids, interest_attributes + ) + + task_attrs = attrs_by_obj_id.get(task_object_id) + if not task_attrs: + return + + for key in interest_attributes: + if key not in hier_attrs: + task_attrs.pop(key, None) + + elif key not in task_attrs: + hier_attrs.pop(key) + + if not task_attrs: + return + + attr_key_by_id = {} + nonhier_id_by_key = {} + hier_attr_ids = [] + for key, attr_id in hier_attrs.items(): + attr_key_by_id[attr_id] = key + hier_attr_ids.append(attr_id) + + conf_ids = list(hier_attr_ids) + for key, attr_id in task_attrs.items(): + attr_key_by_id[attr_id] = key + nonhier_id_by_key[key] = attr_id + conf_ids.append(attr_id) + + result = query_custom_attributes( + session, conf_ids, whole_hierarchy_ids + ) + hier_values_by_entity_id = { + entity_id: {} + for entity_id in whole_hierarchy_ids + } + values_by_entity_id = { + entity_id: { + attr_id: None + for attr_id in conf_ids + } + for entity_id in whole_hierarchy_ids + } + for item in result: + attr_id = item["configuration_id"] + entity_id = item["entity_id"] + value = item["value"] + + values_by_entity_id[entity_id][attr_id] = value + + if attr_id in hier_attr_ids and value is not None: + hier_values_by_entity_id[entity_id][attr_id] = value + + for task_id in tuple(task_ids): + for attr_id in hier_attr_ids: + entity_ids = [] + value = None + entity_id = task_id + while value is None: + entity_value = hier_values_by_entity_id[entity_id] + if attr_id in entity_value: + value = entity_value[attr_id] + if value is None: + break + + if value is None: + entity_ids.append(entity_id) + + entity_id = parent_id_by_entity_id.get(entity_id) + if entity_id is None: + break + + for entity_id in entity_ids: + hier_values_by_entity_id[entity_id][attr_id] = value + + changes = [] + for task_id in tuple(task_ids): + parent_id = parent_id_by_entity_id[task_id] + for attr_id in hier_attr_ids: + attr_key = attr_key_by_id[attr_id] + nonhier_id = nonhier_id_by_key[attr_key] + + # Real value of hierarchical attribute on parent + # - If is none then should be unset + real_parent_value = values_by_entity_id[parent_id][attr_id] + # Current hierarchical value of a task + # - Will be compared to real parent value + hier_value = hier_values_by_entity_id[task_id][attr_id] + + # Parent value that can be inherited from it's parent entity + parent_value = hier_values_by_entity_id[parent_id][attr_id] + # Task value of nonhierarchical custom attribute + nonhier_value = values_by_entity_id[task_id][nonhier_id] + + if real_parent_value != hier_value: + changes.append({ + "new_value": real_parent_value, + "attr_id": attr_id, + "entity_id": task_id, + "attr_key": attr_key + }) + + if parent_value != nonhier_value: + changes.append({ + "new_value": parent_value, + "attr_id": nonhier_id, + "entity_id": task_id, + "attr_key": attr_key + }) + + self._commit_changes(session, changes) + def _commit_changes(self, session, changes): uncommited_changes = False for idx, item in enumerate(changes): From 9fa01ac76ea1b74e76c8cc99195af9e05cefbd97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:43:55 +0200 Subject: [PATCH 194/359] object type ids preparation is at one place --- .../event_push_frame_values_to_task.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 84f26dc57a..3ee148b3ed 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -208,13 +208,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): self.join_query_keys(matching_parent_ids) ) ) - object_type_ids = set() - for entity in entities: - object_type_ids.add(entity["object_type_id"]) # Prepare task object id task_object_id = object_types_by_name["task"]["id"] + object_type_ids = set() object_type_ids.add(task_object_id) + for entity in entities: + object_type_ids.add(entity["object_type_id"]) attrs_by_obj_id, hier_attrs = self.attrs_configurations( session, object_type_ids, interest_attributes From 877b5b853548099a3fad09551e681dc7423d1959 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:44:13 +0200 Subject: [PATCH 195/359] added few comments --- .../event_push_frame_values_to_task.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 3ee148b3ed..d1393796ff 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -168,24 +168,42 @@ class PushFrameValuesToTaskEvent(BaseEvent): self, session, object_types_by_name, task_parent_changes, interest_entity_types, interest_attributes ): + """Push custom attribute values if task parent has changed. + + Parent is changed if task is created or if is moved under different + entity. We don't care about all task changes only about those that + have it's parent in interest types (from settings). + + Tasks hierarchical value should be unset or set based on parents + real hierarchical value and non hierarchical custom attribute value + should be set to hierarchical value. + """ + # Store task ids which were created or moved under parent with entity + # type defined in settings (interest_entity_types). task_ids = set() + # Store parent ids of matching task ids matching_parent_ids = set() + # Store all entity ids of all entities to be able query hierarchical + # values. whole_hierarchy_ids = set() + # Store parent id of each entity id parent_id_by_entity_id = {} for entity_info in task_parent_changes: - parents = entity_info.get("parents") or [] # Ignore entities with less parents than 2 # NOTE entity itself is also part of "parents" value + parents = entity_info.get("parents") or [] if len(parents) < 2: continue parent_info = parents[1] + # Check if parent has entity type we care about. if parent_info["entity_type"] not in interest_entity_types: continue task_ids.add(entity_info["entityId"]) matching_parent_ids.add(parent_info["entityId"]) + # Store whole hierarchi of task entity prev_id = None for item in parents: item_id = item["entityId"] @@ -200,9 +218,12 @@ class PushFrameValuesToTaskEvent(BaseEvent): break prev_id = item_id + # Just skip if nothing is interesting for our settings if not matching_parent_ids: return + # Query object type ids of parent ids for custom attribute + # definitions query entities = session.query( "select object_type_id from TypedContext where id in ({})".format( self.join_query_keys(matching_parent_ids) @@ -211,6 +232,8 @@ class PushFrameValuesToTaskEvent(BaseEvent): # Prepare task object id task_object_id = object_types_by_name["task"]["id"] + + # All object ids for which we're querying custom attribute definitions object_type_ids = set() object_type_ids.add(task_object_id) for entity in entities: @@ -220,10 +243,13 @@ class PushFrameValuesToTaskEvent(BaseEvent): session, object_type_ids, interest_attributes ) + # Skip if all task attributes are not available task_attrs = attrs_by_obj_id.get(task_object_id) if not task_attrs: return + # Skip attributes that is not in both hierarchical and nonhierarchical + # TODO be able to push values if hierarchical is available for key in interest_attributes: if key not in hier_attrs: task_attrs.pop(key, None) @@ -231,9 +257,11 @@ class PushFrameValuesToTaskEvent(BaseEvent): elif key not in task_attrs: hier_attrs.pop(key) + # Skip if nothing remained if not task_attrs: return + # Do some preparations for custom attribute values query attr_key_by_id = {} nonhier_id_by_key = {} hier_attr_ids = [] @@ -247,13 +275,21 @@ class PushFrameValuesToTaskEvent(BaseEvent): nonhier_id_by_key[key] = attr_id conf_ids.append(attr_id) + # Query custom attribute values + # - result does not contain values for all entities only result of + # query callback to ftrack server result = query_custom_attributes( session, conf_ids, whole_hierarchy_ids ) + + # Prepare variables where result will be stored + # - hierachical values should not contain attribute with value by + # default hier_values_by_entity_id = { entity_id: {} for entity_id in whole_hierarchy_ids } + # - real values of custom attributes values_by_entity_id = { entity_id: { attr_id: None @@ -271,6 +307,10 @@ class PushFrameValuesToTaskEvent(BaseEvent): if attr_id in hier_attr_ids and value is not None: hier_values_by_entity_id[entity_id][attr_id] = value + # Prepare values for all task entities + # - going through all parents and storing first value value + # - store None to those that are already known that do not have set + # value at all for task_id in tuple(task_ids): for attr_id in hier_attr_ids: entity_ids = [] @@ -293,6 +333,7 @@ class PushFrameValuesToTaskEvent(BaseEvent): for entity_id in entity_ids: hier_values_by_entity_id[entity_id][attr_id] = value + # Prepare changes to commit changes = [] for task_id in tuple(task_ids): parent_id = parent_id_by_entity_id[task_id] From 1be4c4fc7f899f2aecec50530f609072bd12edc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:45:33 +0200 Subject: [PATCH 196/359] added some more comments --- .../event_handlers_server/event_push_frame_values_to_task.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index d1393796ff..1d64174188 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -151,6 +151,9 @@ class PushFrameValuesToTaskEvent(BaseEvent): name_low = object_type["name"].lower() object_types_by_name[name_low] = object_type + # NOTE it would be nice to check if `interesting_data` do not contain + # value changs of tasks that were created or moved + # - it is a complex way how to find out if interesting_data: self.process_attribute_changes( session, object_types_by_name, From 5cda53abffe2eed91c2bf5c09d8b3a5e559ab702 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 24 Jun 2021 20:58:30 +0200 Subject: [PATCH 197/359] keep refresh button available even if not in dev mode --- openpype/tools/settings/settings/categories.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 34ab4c464a..392c749211 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -183,6 +183,12 @@ class SettingsCategoryWidget(QtWidgets.QWidget): footer_widget = QtWidgets.QWidget(configurations_widget) footer_layout = QtWidgets.QHBoxLayout(footer_widget) + refresh_icon = qtawesome.icon("fa.refresh", color="white") + refresh_btn = QtWidgets.QPushButton(footer_widget) + refresh_btn.setIcon(refresh_icon) + + footer_layout.addWidget(refresh_btn, 0) + if self.user_role == "developer": self._add_developer_ui(footer_layout) @@ -205,8 +211,10 @@ class SettingsCategoryWidget(QtWidgets.QWidget): main_layout.addWidget(configurations_widget, 1) save_btn.clicked.connect(self._save) + refresh_btn.clicked.connect(self._on_refresh) self.save_btn = save_btn + self.refresh_btn = refresh_btn self.require_restart_label = require_restart_label self.scroll_widget = scroll_widget self.content_layout = content_layout @@ -220,10 +228,6 @@ class SettingsCategoryWidget(QtWidgets.QWidget): return def _add_developer_ui(self, footer_layout): - refresh_icon = qtawesome.icon("fa.refresh", color="white") - refresh_button = QtWidgets.QPushButton() - refresh_button.setIcon(refresh_icon) - modify_defaults_widget = QtWidgets.QWidget() modify_defaults_checkbox = QtWidgets.QCheckBox(modify_defaults_widget) modify_defaults_checkbox.setChecked(self._hide_studio_overrides) @@ -235,10 +239,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): modify_defaults_layout.addWidget(label_widget) modify_defaults_layout.addWidget(modify_defaults_checkbox) - footer_layout.addWidget(refresh_button, 0) footer_layout.addWidget(modify_defaults_widget, 0) - refresh_button.clicked.connect(self._on_refresh) modify_defaults_checkbox.stateChanged.connect( self._on_modify_defaults ) From d64e1d249d4eb3042dc89890553d0e536c06c2de Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 11:20:55 +0200 Subject: [PATCH 198/359] width of workfile toos widget have better sizes to display content --- openpype/tools/workfiles/app.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index c79e55a143..d567e26d74 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -944,10 +944,8 @@ class Window(QtWidgets.QMainWindow): split_widget.addWidget(tasks_widget) split_widget.addWidget(files_widget) split_widget.addWidget(side_panel) - split_widget.setStretchFactor(0, 1) - split_widget.setStretchFactor(1, 1) - split_widget.setStretchFactor(2, 3) - split_widget.setStretchFactor(3, 1) + split_widget.setSizes([255, 160, 455, 175]) + body_layout.addWidget(split_widget) # Add top margin for tasks to align it visually with files as @@ -976,7 +974,7 @@ class Window(QtWidgets.QMainWindow): # Force focus on the open button by default, required for Houdini. files_widget.btn_open.setFocus() - self.resize(1000, 600) + self.resize(1200, 600) def keyPressEvent(self, event): """Custom keyPressEvent. From 7b273f15497b0980c6a9cf1717882b6ea933188e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 12:37:26 +0200 Subject: [PATCH 199/359] fix project specific environment variables to work as expected --- openpype/lib/applications.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index a7dcb6dd55..9866400928 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1159,6 +1159,9 @@ def prepare_host_environments(data, implementation_envs=True): def apply_project_environments_value(project_name, env, project_settings=None): """Apply project specific environments on passed environments. + The enviornments are applied on passed `env` argument value so it is not + required to apply changes back. + Args: project_name (str): Name of project for which environemnts should be received. @@ -1167,6 +1170,9 @@ def apply_project_environments_value(project_name, env, project_settings=None): project_settings (dict): Project settings for passed project name. Optional if project settings are already prepared. + Returns: + dict: Passed env values with applied project environments. + Raises: KeyError: If project settings do not contain keys for project specific environments. @@ -1177,10 +1183,9 @@ def apply_project_environments_value(project_name, env, project_settings=None): project_settings = get_project_settings(project_name) env_value = project_settings["global"]["project_environments"] - if not env_value: - return env - parsed = acre.parse(env_value) - return _merge_env(parsed, env) + if env_value: + env.update(_merge_env(acre.parse(env_value), env)) + return env def prepare_context_environments(data): @@ -1209,9 +1214,8 @@ def prepare_context_environments(data): # Load project specific environments project_name = project_doc["name"] - data["env"] = apply_project_environments_value( - project_name, data["env"] - ) + # Apply project specific environments on current env value + apply_project_environments_value(project_name, data["env"]) app = data["app"] workdir_data = get_workdir_data( From fab1ed955313f810b26b47ba982bdf1d9c83e658 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 13:19:16 +0200 Subject: [PATCH 200/359] move environment filling before mac specific check --- openpype/lib/applications.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index a7dcb6dd55..c8380dd3e0 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -449,6 +449,12 @@ class ApplicationExecutable: """Representation of executable loaded from settings.""" def __init__(self, executable): + # Try to format executable with environments + try: + executable = executable.format(**os.environ) + except Exception: + pass + # On MacOS check if exists path to executable when ends with `.app` # - it is common that path will lead to "/Applications/Blender" but # real path is "/Applications/Blender.app" @@ -460,12 +466,6 @@ class ApplicationExecutable: if os.path.exists(_executable): executable = _executable - # Try to format executable with environments - try: - executable = executable.format(**os.environ) - except Exception: - pass - self.executable_path = executable def __str__(self): From 251d3add0162aa3f883daa53fe366158103e843a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:51:47 +0200 Subject: [PATCH 201/359] added root_key as abstract property to BaseEntity --- openpype/settings/entities/base_entity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index c6bff1ff47..147bd613d1 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -279,6 +279,11 @@ class BaseItemEntity(BaseEntity): self, "Dynamic entity can't require restart." ) + @abstractproperty + def root_key(self): + """Root is represented as this dictionary key.""" + pass + @abstractmethod def set_override_state(self, state): """Set override state and trigger it on children. From ac36919e2642e27db2fce2042aed61b27f04a061 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:03 +0200 Subject: [PATCH 202/359] added root_key to both root entities --- openpype/settings/entities/root_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 401d3980c9..c637da8f76 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -491,6 +491,8 @@ class SystemSettings(RootEntity): schema_data (dict): Pass schema data to entity. This is for development and debugging purposes. """ + root_key = SYSTEM_SETTINGS_KEY + def __init__( self, set_studio_state=True, reset=True, schema_data=None ): @@ -600,6 +602,8 @@ class ProjectSettings(RootEntity): schema_data (dict): Pass schema data to entity. This is for development and debugging purposes. """ + root_key = PROJECT_SETTINGS_KEY + def __init__( self, project_name=None, From 81bebe03c675c1a4274a963b5d6653e47667560f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:16 +0200 Subject: [PATCH 203/359] implemented root_key propery for rest of entities --- openpype/settings/entities/base_entity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 147bd613d1..1b0dd372fa 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -871,6 +871,10 @@ class ItemEntity(BaseItemEntity): """Call save on root item.""" self.root_item.save() + @property + def root_key(self): + return self.root_item.root_key + def schema_validations(self): if not self.label and self.use_label_wrap: reason = ( From 5c9016c676364097fc3d2de8450bbe95eb45b3d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:52:31 +0200 Subject: [PATCH 204/359] implemented get_entity_from_path for system settings --- openpype/settings/entities/root_entities.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index c637da8f76..5ed78fd401 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -189,11 +189,10 @@ class RootEntity(BaseItemEntity): if not KEY_REGEX.match(key): raise InvalidKeySymbols(self.path, key) + @abstractmethod def get_entity_from_path(self, path): - """Return system settings entity.""" - raise NotImplementedError(( - "Method `get_entity_from_path` not available for \"{}\"" - ).format(self.__class__.__name__)) + """Return entity matching passed path.""" + pass def create_schema_object(self, schema_data, *args, **kwargs): """Create entity by entered schema data. @@ -505,6 +504,18 @@ class SystemSettings(RootEntity): if set_studio_state: self.set_studio_state() + def get_entity_from_path(self, path): + """Return system settings entity.""" + path_parts = path.split("/") + first_part = path_parts[0] + output = self + if first_part == self.root_key: + path_parts.pop(0) + + for path_part in path_parts: + output = output[path_part] + return output + def _reset_values(self): default_value = get_default_settings()[SYSTEM_SETTINGS_KEY] for key, child_obj in self.non_gui_children.items(): From ee72605f5855737b6d7905dd1ec80a393b7caf48 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:53:30 +0200 Subject: [PATCH 205/359] implemented copy action in settings --- openpype/tools/settings/settings/base.py | 55 ++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 03f920b7dc..c0ef968247 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -1,3 +1,5 @@ +import json + from Qt import QtWidgets, QtGui, QtCore from openpype.tools.settings import CHILD_OFFSET from .widgets import ExpandingWidget @@ -125,6 +127,58 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = remove_from_project_override menu.addAction(action) + def _copy_value_action(self, menu, actions_mapping): + def copy_value(): + mime_data = QtCore.QMimeData() + + if self.entity.is_dynamic_item or self.entity.is_in_dynamic_item: + entity_path = None + else: + entity_path = "/".join( + [self.entity.root_key, self.entity.path] + ) + + value = self.entity.value + # Copy for settings tool + settings_data = { + "root_key": self.entity.root_key, + "value": value, + "path": entity_path + } + settings_encoded_data = QtCore.QByteArray() + settings_stream = QtCore.QDataStream( + settings_encoded_data, QtCore.QIODevice.WriteOnly + ) + settings_stream.writeQString(json.dumps(settings_data)) + mime_data.setData( + "application/copy_settings_value", settings_encoded_data + ) + + # Copy as json + json_encoded_data = None + if isinstance(value, (dict, list)): + json_encoded_data = QtCore.QByteArray() + json_stream = QtCore.QDataStream( + json_encoded_data, QtCore.QIODevice.WriteOnly + ) + json_stream.writeQString(json.dumps(value)) + + mime_data.setData("application/json", json_encoded_data) + + # Copy as text + if json_encoded_data is None: + # Store value as string + mime_data.setText(str(value)) + else: + # Store data as json string + mime_data.setText(json.dumps(value, indent=4)) + + QtWidgets.QApplication.clipboard().setMimeData(mime_data) + + action = QtWidgets.QAction("Copy") + actions_mapping[action] = copy_value + menu.addAction(action) + def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: return @@ -143,6 +197,7 @@ class BaseWidget(QtWidgets.QWidget): self._remove_from_studio_default_action(menu, actions_mapping) self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) + self._copy_value_action(menu, actions_mapping) if not actions_mapping: action = QtWidgets.QAction("< No action >") From 8eeeda11e7b4058063bd14ab2789e4b9bfad08d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 14:53:42 +0200 Subject: [PATCH 206/359] implemented base of paste value actions --- openpype/tools/settings/settings/base.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index c0ef968247..8882cc0c46 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -179,6 +179,46 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = copy_value menu.addAction(action) + def _paste_value_action(self, menu, actions_mapping): + mime_data = QtWidgets.QApplication.clipboard().mimeData() + mime_value = mime_data.data("application/copy_settings_value") + if not mime_value: + return + + settings_stream = QtCore.QDataStream( + mime_value, QtCore.QIODevice.ReadOnly + ) + mime_data_value_str = settings_stream.readQString() + mime_data_value = json.loads(mime_data_value_str) + + value = mime_data_value["value"] + path = mime_data_value["path"] + root_key = mime_data_value["root_key"] + + def paste_value(): + try: + self.entity.set(value) + except Exception: + # TODO show dialog + print("Failed") + import sys + import traceback + + traceback.print_exception(*sys.exc_info()) + + def paste_value_to_path(): + entity = self.entity.get_entity_from_path(path) + entity.set(value) + + if path and root_key == self.entity.root_key: + action = QtWidgets.QAction("Paste to same entity") + actions_mapping[action] = paste_value_to_path + menu.addAction(action) + + action = QtWidgets.QAction("Paste") + actions_mapping[action] = paste_value + menu.addAction(action) + def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: return @@ -198,6 +238,7 @@ class BaseWidget(QtWidgets.QWidget): self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) self._copy_value_action(menu, actions_mapping) + self._paste_value_action(menu, actions_mapping) if not actions_mapping: action = QtWidgets.QAction("< No action >") From 2e8df3e2d0339cd1ebf6469af800e26d004527db Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:08:29 +0200 Subject: [PATCH 207/359] show dialog if paste crashes --- openpype/tools/settings/settings/base.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8882cc0c46..09a36cd99b 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -199,12 +199,14 @@ class BaseWidget(QtWidgets.QWidget): try: self.entity.set(value) except Exception: - # TODO show dialog - print("Failed") - import sys - import traceback - - traceback.print_exception(*sys.exc_info()) + dialog = QtWidgets.QMessageBox(self) + dialog.setWindowTitle("Value does not match settings schema") + dialog.setIcon(QtWidgets.QMessageBox.Warning) + dialog.setText(( + "Pasted value does not seem to match schema of destination" + " settings entity." + )) + dialog.exec_() def paste_value_to_path(): entity = self.entity.get_entity_from_path(path) From 455acfaae9cee59b6a864b1c7fae0732ba55faef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:10:43 +0200 Subject: [PATCH 208/359] paste_value_to_path is simplified --- openpype/tools/settings/settings/base.py | 29 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 09a36cd99b..40a6562fd0 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -195,6 +195,26 @@ class BaseWidget(QtWidgets.QWidget): path = mime_data_value["path"] root_key = mime_data_value["root_key"] + # Try to find matching entity to be able paste values to same spot + # - entity can't by dynamic or in dynamic item + # - must be in same root entity as source copy + # Can't copy system settings <-> project settings + matching_entity = None + if path and root_key == self.entity.root_key: + try: + matching_entity = self.entity.get_entity_from_path(path) + except Exception: + pass + + # Paste value to matchin entity + def paste_value_to_path(): + matching_entity.set(value) + + if matching_entity is not None: + action = QtWidgets.QAction("Paste to same entity", menu) + actions_mapping[action] = paste_value_to_path + menu.addAction(action) + def paste_value(): try: self.entity.set(value) @@ -208,15 +228,6 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() - def paste_value_to_path(): - entity = self.entity.get_entity_from_path(path) - entity.set(value) - - if path and root_key == self.entity.root_key: - action = QtWidgets.QAction("Paste to same entity") - actions_mapping[action] = paste_value_to_path - menu.addAction(action) - action = QtWidgets.QAction("Paste") actions_mapping[action] = paste_value menu.addAction(action) From 302c6b44b107085616f7522ab91c94fac5da249f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:11:18 +0200 Subject: [PATCH 209/359] copy/paste actions are separated with separator in menu --- openpype/tools/settings/settings/base.py | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 40a6562fd0..8986780f31 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -127,7 +127,7 @@ class BaseWidget(QtWidgets.QWidget): actions_mapping[action] = remove_from_project_override menu.addAction(action) - def _copy_value_action(self, menu, actions_mapping): + def _copy_value_actions(self, menu): def copy_value(): mime_data = QtCore.QMimeData() @@ -175,15 +175,15 @@ class BaseWidget(QtWidgets.QWidget): QtWidgets.QApplication.clipboard().setMimeData(mime_data) - action = QtWidgets.QAction("Copy") - actions_mapping[action] = copy_value - menu.addAction(action) + action = QtWidgets.QAction("Copy", menu) + return [(action, copy_value)] - def _paste_value_action(self, menu, actions_mapping): + def _paste_value_actions(self, menu): + output = [] mime_data = QtWidgets.QApplication.clipboard().mimeData() mime_value = mime_data.data("application/copy_settings_value") if not mime_value: - return + return output settings_stream = QtCore.QDataStream( mime_value, QtCore.QIODevice.ReadOnly @@ -212,8 +212,7 @@ class BaseWidget(QtWidgets.QWidget): if matching_entity is not None: action = QtWidgets.QAction("Paste to same entity", menu) - actions_mapping[action] = paste_value_to_path - menu.addAction(action) + output.append((action, paste_value_to_path)) def paste_value(): try: @@ -229,8 +228,9 @@ class BaseWidget(QtWidgets.QWidget): dialog.exec_() action = QtWidgets.QAction("Paste") - actions_mapping[action] = paste_value - menu.addAction(action) + output.append((action, paste_value)) + + return output def show_actions_menu(self, event=None): if event and event.button() != QtCore.Qt.RightButton: @@ -250,8 +250,15 @@ class BaseWidget(QtWidgets.QWidget): self._remove_from_studio_default_action(menu, actions_mapping) self._add_to_project_override_action(menu, actions_mapping) self._remove_from_project_override_action(menu, actions_mapping) - self._copy_value_action(menu, actions_mapping) - self._paste_value_action(menu, actions_mapping) + + ui_actions = [] + ui_actions.extend(self._copy_value_actions(menu)) + ui_actions.extend(self._paste_value_actions(menu)) + if ui_actions: + menu.addSeparator() + for action, callback in ui_actions: + menu.addAction(action) + actions_mapping[action] = callback if not actions_mapping: action = QtWidgets.QAction("< No action >") From daf12bb9736ca9c7fae575033c756deb14121b8a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:11:23 +0200 Subject: [PATCH 210/359] added comment --- openpype/tools/settings/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 8986780f31..ac040f9e25 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -214,6 +214,7 @@ class BaseWidget(QtWidgets.QWidget): action = QtWidgets.QAction("Paste to same entity", menu) output.append((action, paste_value_to_path)) + # Simple paste value method def paste_value(): try: self.entity.set(value) From 9c02ecb35aadb1ecae8fb12abf183d4491e839c6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:14:56 +0200 Subject: [PATCH 211/359] make both paste secure with dialog poopup --- openpype/tools/settings/settings/base.py | 27 ++++++++++++++---------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index ac040f9e25..620628d1d2 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -180,8 +180,10 @@ class BaseWidget(QtWidgets.QWidget): def _paste_value_actions(self, menu): output = [] + # Allow paste of value only if were copied from this UI mime_data = QtWidgets.QApplication.clipboard().mimeData() mime_value = mime_data.data("application/copy_settings_value") + # Skip if there is nothing to do if not mime_value: return output @@ -206,18 +208,9 @@ class BaseWidget(QtWidgets.QWidget): except Exception: pass - # Paste value to matchin entity - def paste_value_to_path(): - matching_entity.set(value) - - if matching_entity is not None: - action = QtWidgets.QAction("Paste to same entity", menu) - output.append((action, paste_value_to_path)) - - # Simple paste value method - def paste_value(): + def _set_entity_value(_entity, _value): try: - self.entity.set(value) + _entity.set(_value) except Exception: dialog = QtWidgets.QMessageBox(self) dialog.setWindowTitle("Value does not match settings schema") @@ -228,6 +221,18 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() + # Paste value to matchin entity + def paste_value_to_path(): + _set_entity_value(matching_entity, value) + + if matching_entity is not None: + action = QtWidgets.QAction("Paste to same entity", menu) + output.append((action, paste_value_to_path)) + + # Simple paste value method + def paste_value(): + _set_entity_value(self.entity, value) + action = QtWidgets.QAction("Paste") output.append((action, paste_value)) From 82ac355a3823cd6be24da8e42411748a7c2ee52f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:23:39 +0200 Subject: [PATCH 212/359] moved simple Paste before special Paste --- openpype/tools/settings/settings/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 620628d1d2..543551b6a2 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -221,6 +221,13 @@ class BaseWidget(QtWidgets.QWidget): )) dialog.exec_() + # Simple paste value method + def paste_value(): + _set_entity_value(self.entity, value) + + action = QtWidgets.QAction("Paste", menu) + output.append((action, paste_value)) + # Paste value to matchin entity def paste_value_to_path(): _set_entity_value(matching_entity, value) @@ -229,13 +236,6 @@ class BaseWidget(QtWidgets.QWidget): action = QtWidgets.QAction("Paste to same entity", menu) output.append((action, paste_value_to_path)) - # Simple paste value method - def paste_value(): - _set_entity_value(self.entity, value) - - action = QtWidgets.QAction("Paste") - output.append((action, paste_value)) - return output def show_actions_menu(self, event=None): From c86ef80326af322ff9a83f4eb5718497d1fdc471 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 25 Jun 2021 15:23:54 +0200 Subject: [PATCH 213/359] changed label to "Paste to same place" --- openpype/tools/settings/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index 543551b6a2..eb5f82ab9a 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -233,7 +233,7 @@ class BaseWidget(QtWidgets.QWidget): _set_entity_value(matching_entity, value) if matching_entity is not None: - action = QtWidgets.QAction("Paste to same entity", menu) + action = QtWidgets.QAction("Paste to same place", menu) output.append((action, paste_value_to_path)) return output From 9dfbd8d7f2d75ad78201e045c9300eaadc647c79 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 26 Jun 2021 03:40:30 +0000 Subject: [PATCH 214/359] [Automated] Bump version --- CHANGELOG.md | 10 ++++++++-- openpype/version.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fe2ce33cb..96b90cd53e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # Changelog -## [3.2.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) @@ -31,9 +32,12 @@ - Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) - Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) - Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) -- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) +**Merged pull requests:** + +- TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) + ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4) @@ -98,6 +102,7 @@ - Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) +- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) - Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) @@ -111,6 +116,7 @@ - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) +- Use poetry to build / publish OpenPype wheel [\#1636](https://github.com/pypeclub/OpenPype/pull/1636) # Changelog diff --git a/openpype/version.py b/openpype/version.py index ce6cfec003..fcd3b2afca 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.3" +__version__ = "3.2.0-nightly.4" From c69b20930c92281bc5b6bc2d4e821855f6612c0b Mon Sep 17 00:00:00 2001 From: Derek Severin Date: Sun, 27 Jun 2021 17:31:57 +0700 Subject: [PATCH 215/359] Minor doc fixes --- website/docs/dev_build.md | 4 ++-- website/docs/module_ftrack.md | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/website/docs/dev_build.md b/website/docs/dev_build.md index 2c4bd1e9af..b3e0c24fc2 100644 --- a/website/docs/dev_build.md +++ b/website/docs/dev_build.md @@ -137,12 +137,12 @@ $ pyenv install -v 3.7.10 $ cd /path/to/pype-3 # set local python version -$ pyenv local 3.7.9 +$ pyenv local 3.7.10 ``` :::note Install build requirements for **Ubuntu** ```shell -sudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git +sudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev git patchelf ``` In case you run in error about `xcb` when running Pype, diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index bd0dbaef4f..9911dee45a 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -34,7 +34,7 @@ To prepare Ftrack for working with OpenPype you'll need to run [OpenPype Admin - Ftrack Event Server is the key to automation of many tasks like _status change_, _thumbnail update_, _automatic synchronization to Avalon database_ and many more. Event server should run at all times to perform the required processing as it is not possible to catch some of them retrospectively with enough certainty. ### Running event server -There are specific launch arguments for event server. With `openpype eventserver` you can launch event server but without prior preparation it will terminate immediately. The reason is that event server requires 3 pieces of information: _Ftrack server url_, _paths to events_ and _credentials (Username and API key)_. Ftrack server URL and Event path are set from OpenPype's environments by default, but the credentials must be done separatelly for security reasons. +There are specific launch arguments for event server. With `openpype_console eventserver` you can launch event server but without prior preparation it will terminate immediately. The reason is that event server requires 3 pieces of information: _Ftrack server url_, _paths to events_ and _credentials (Username and API key)_. Ftrack server URL and Event path are set from OpenPype's environments by default, but the credentials must be done separatelly for security reasons. @@ -56,7 +56,7 @@ There are specific launch arguments for event server. With `openpype eventserver - `--ftrack-url "https://yourdomain.ftrackapp.com/"` : Ftrack server URL _(it is not needed to enter if you have set `FTRACK_SERVER` in OpenPype' environments)_ - `--ftrack-events-path "//Paths/To/Events/"` : Paths to events folder. May contain multiple paths separated by `;`. _(it is not needed to enter if you have set `FTRACK_EVENTS_PATH` in OpenPype' environments)_ -So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype.exe eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype.exe eventserver`. +So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype_console.exe eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype_console.exe eventserver`. @@ -100,14 +100,17 @@ Event server should **not** run more than once! It may cause major issues. - create file: - `sudo vi /opt/OpenPype/run_event_server.sh` + `sudo vi /opt/openpype/run_event_server.sh` - add content to the file: ```sh -#!\usr\bin\env +#!/usr/bin/env export OPENPYPE_DEBUG=3 pushd /mnt/pipeline/prod/openpype-setup -. openpype eventserver --ftrack-user --ftrack-api-key +. openpype_console eventserver --ftrack-user --ftrack-api-key ``` +- change file permission: + `sudo chmod 0755 /opt/openpype/run_event_server.sh` + - create service file: `sudo vi /etc/systemd/system/openpype-ftrack-event-server.service` - add content to the service file @@ -145,7 +148,7 @@ WantedBy=multi-user.target @echo off set OPENPYPE_DEBUG=3 pushd \\path\to\file\ -call openpype.bat eventserver --ftrack-user --ftrack-api-key +call openpype_console.exe eventserver --ftrack-user --ftrack-api-key ``` - download and install `nssm.cc` - create Windows service according to nssm.cc manual From c1d147934e8af6a600e8748f99b88a51d677d4b1 Mon Sep 17 00:00:00 2001 From: Derek Severin Date: Sun, 27 Jun 2021 17:55:16 +0700 Subject: [PATCH 216/359] Removed 'call' --- website/docs/module_ftrack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index 9911dee45a..6d56277c67 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -148,7 +148,7 @@ WantedBy=multi-user.target @echo off set OPENPYPE_DEBUG=3 pushd \\path\to\file\ -call openpype_console.exe eventserver --ftrack-user --ftrack-api-key +openpype_console.exe eventserver --ftrack-user --ftrack-api-key ``` - download and install `nssm.cc` - create Windows service according to nssm.cc manual From 02def9660b4c30937b70127e7cdd725fca767944 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 28 Jun 2021 09:56:08 +0200 Subject: [PATCH 217/359] Fix - single file files are str only, cast it to list to count properly --- .../plugins/publish/validate_frame_ranges.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py index e3086fb638..943cb73b98 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py @@ -43,7 +43,10 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): self.log.warning("Cannot check for extension {}".format(ext)) return - frames = len(instance.data.get("representations", [None])[0]["files"]) + files = instance.data.get("representations", [None])[0]["files"] + if isinstance(files, str): + files = [files] + frames = len(files) err_msg = "Frame duration from DB:'{}' ". format(int(duration)) +\ " doesn't match number of files:'{}'".format(frames) +\ From 50cf8f96143eb44612723b9f0e3057be290df9ba Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 14:16:38 +0200 Subject: [PATCH 218/359] fix object attributes error --- .../event_handlers_server/event_push_frame_values_to_task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py index 1d64174188..81719258e1 100644 --- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py +++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py @@ -124,8 +124,8 @@ class PushFrameValuesToTaskEvent(BaseEvent): )) return - interest_attributes = set(self.interest_attributes) - interest_entity_types = set(self.interest_entity_types) + interest_attributes = set(interest_attributes) + interest_entity_types = set(interest_entity_types) # Separate value changes and task parent changes _entities_info = [] From c98aafea8ef5f18bd31bdd53c13c52a299395b9b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:35:34 +0200 Subject: [PATCH 219/359] base of dict contitional --- .../settings/entities/dict_conditional.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 openpype/settings/entities/dict_conditional.py diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py new file mode 100644 index 0000000000..01da57a190 --- /dev/null +++ b/openpype/settings/entities/dict_conditional.py @@ -0,0 +1,33 @@ +import copy +import collections + +from .lib import ( + WRAPPER_TYPES, + OverrideState, + NOT_SET +) +from openpype.settings.constants import ( + METADATA_KEYS, + M_OVERRIDEN_KEY, + KEY_REGEX +) +from . import ( + BaseItemEntity, + ItemEntity, + BoolEntity, + GUIEntity +) +from .exceptions import ( + SchemaDuplicatedKeys, + EntitySchemaError, + InvalidKeySymbols +) + + +class DictConditionalEntity(ItemEntity): + schema_types = ["dict-conditional"] + _default_label_wrap = { + "use_label_wrap": False, + "collapsible": False, + "collapsed": True + } From 6c63bc048fe2e12f82581a14797dc8776d65f46e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:35:50 +0200 Subject: [PATCH 220/359] added example schema for reference --- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 01da57a190..8e7a7b79c9 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -24,6 +24,52 @@ from .exceptions import ( ) +example_schema = { + "type": "dict-conditional", + "key": "KEY", + "label": "LABEL", + "enum_key": "type", + "enum_label": "label", + "enum_children": [ + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "command", + "label": "Comand" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "type": "list", + "object_type": "text" + } + ] + }, + { + "key": "separator", + "label": "Separator" + } + ] +} + + class DictConditionalEntity(ItemEntity): schema_types = ["dict-conditional"] _default_label_wrap = { From 1063f8210ab0ec1c8e7c41493408935023f88cda Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:36:38 +0200 Subject: [PATCH 221/359] implemented `_item_initalization` similar to 'dict' entity --- .../settings/entities/dict_conditional.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8e7a7b79c9..989d69a290 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -77,3 +77,33 @@ class DictConditionalEntity(ItemEntity): "collapsible": False, "collapsed": True } + + def _item_initalization(self): + self._default_metadata = NOT_SET + self._studio_override_metadata = NOT_SET + self._project_override_metadata = NOT_SET + + self._ignore_child_changes = False + + # `current_metadata` are still when schema is loaded + # - only metadata stored with dict item are gorup overrides in + # M_OVERRIDEN_KEY + self._current_metadata = {} + self._metadata_are_modified = False + + # Children are stored by key as keys are immutable and are defined by + # schema + self.valid_value_types = (dict, ) + self.children = collections.defaultdict(list) + self.non_gui_children = collections.defaultdict(dict) + self.gui_layout = collections.defaultdict(list) + + if self.is_dynamic_item: + self.require_key = False + + self.enum_key = self.schema_data.get("enum_key") + self.enum_label = self.schema_data.get("enum_label") + self.enum_children = self.schema_data.get("enum_children") + + self.enum_entity = None + self.current_enum = None From 82f1817ec0ca4fca3cd6efca01b28e28c76b1715 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:39:32 +0200 Subject: [PATCH 222/359] implemented _add_children method --- .../settings/entities/dict_conditional.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 989d69a290..da6df6170d 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -107,3 +107,61 @@ class DictConditionalEntity(ItemEntity): self.enum_entity = None self.current_enum = None + + self._add_children() + + def _add_children(self): + """Add children from schema data and repare enum items. + + Each enum item must have defined it's children. None are shared across + all enum items. + + Nice to have: Have ability to have shared keys across all enum items. + + All children are stored by their enum item. + """ + # Skip and wait for validation + if not self.enum_children or not self.enum_key: + return + + enum_items = [] + valid_enum_items = [] + for item in self.enum_children: + if isinstance(item, dict) and "key" in item: + valid_enum_items.append(item) + + first_key = None + for item in valid_enum_items: + item_key = item["key"] + if first_key is None: + first_key = item_key + item_label = item.get("label") or item_key + enum_items.append({item_key: item_label}) + + if not enum_items: + return + + self.current_enum = first_key + + enum_key = self.enum_key or "invalid" + enum_schema = { + "type": "enum", + "multiselection": False, + "enum_items": enum_items, + "key": enum_key, + "label": self.enum_label or enum_key + } + enum_entity = self.create_schema_object(enum_schema, self) + self.enum_entity = enum_entity + + for item in valid_enum_items: + item_key = item["key"] + children = item.get("children") or [] + for children_schema in children: + child_obj = self.create_schema_object(children_schema, self) + self.children[item_key].append(child_obj) + self.gui_layout[item_key].append(child_obj) + if isinstance(child_obj, GUIEntity): + continue + + self.non_gui_children[item_key][child_obj.key] = child_obj From 694f6b58cb0467aa7ddd496a60a61ef220db31b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:39:54 +0200 Subject: [PATCH 223/359] added schema validations of conditional dictionary --- .../settings/entities/dict_conditional.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index da6df6170d..20ee80337d 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -110,6 +110,67 @@ class DictConditionalEntity(ItemEntity): self._add_children() + def schema_validations(self): + """Validation of schema data.""" + if self.enum_key is None: + raise EntitySchemaError(self, "Key 'enum_key' is not set.") + + if not isinstance(self.enum_children, list): + raise EntitySchemaError( + self, "Key 'enum_children' must be a list. Got: {}".format( + str(type(self.enum_children)) + ) + ) + + if not self.enum_children: + raise EntitySchemaError(self, ( + "Key 'enum_children' have empty value. Entity can't work" + " without children definitions." + )) + + children_def_keys = [] + for children_def in self.enum_children: + if not isinstance(children_def, dict): + raise EntitySchemaError(( + "Children definition under key 'enum_children' must" + " be a dictionary." + )) + + if "key" not in children_def: + raise EntitySchemaError(( + "Children definition under key 'enum_children' miss" + " 'key' definition." + )) + # We don't validate regex of these keys because they will be stored + # as value at the end. + key = children_def["key"] + if key in children_def_keys: + # TODO this hould probably be different exception? + raise SchemaDuplicatedKeys(self, key) + children_def_keys.append(key) + + for children in self.children.values(): + children_keys = set() + children_keys.add(self.enum_key) + for child_entity in children: + if not isinstance(child_entity, BaseItemEntity): + continue + elif child_entity.key not in children_keys: + children_keys.add(child_entity.key) + else: + raise SchemaDuplicatedKeys(self, child_entity.key) + + for children_by_key in self.non_gui_children.values(): + for key in children_by_key.keys(): + if not KEY_REGEX.match(key): + raise InvalidKeySymbols(self.path, key) + + super(DictConditionalEntity, self).schema_validations() + # Trigger schema validation on children entities + for children in self.children.values(): + for child_obj in children: + child_obj.schema_validations() + def _add_children(self): """Add children from schema data and repare enum items. From ff701860f704c186fe969d76042747bd227bb331 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:48:09 +0200 Subject: [PATCH 224/359] implemented `get_child_path` --- .../settings/entities/dict_conditional.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 20ee80337d..79b5624505 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -226,3 +226,24 @@ class DictConditionalEntity(ItemEntity): continue self.non_gui_children[item_key][child_obj.key] = child_obj + + def get_child_path(self, child_obj): + """Get hierarchical path of child entity. + + Child must be entity's direct children. This must be possible to get + for any children even if not from current enum value. + """ + if child_obj is self.enum_entity: + return "/".join([self.path, self.enum_key]) + + result_key = None + for children in self.non_gui_children.values(): + for key, _child_obj in children.items(): + if _child_obj is child_obj: + result_key = key + break + + if result_key is None: + raise ValueError("Didn't found child {}".format(child_obj)) + + return "/".join([self.path, result_key]) From 218158ddc4961e24788c332fb74104678452ed4d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:51:56 +0200 Subject: [PATCH 225/359] implemented base dictionary methods --- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 79b5624505..71e727e53f 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -78,6 +78,52 @@ class DictConditionalEntity(ItemEntity): "collapsed": True } + def __getitem__(self, key): + """Return entity inder key.""" + return self.non_gui_children[self.current_enum][key] + + def __setitem__(self, key, value): + """Set value of item under key.""" + child_obj = self.non_gui_children[self.current_enum][key] + child_obj.set(value) + + def __iter__(self): + """Iter through keys.""" + for key in self.keys(): + yield key + + def __contains__(self, key): + """Check if key is available.""" + return key in self.non_gui_children[self.current_enum] + + def get(self, key, default=None): + """Safe entity getter by key.""" + return self.non_gui_children[self.current_enum].get(key, default) + + def keys(self): + """Entity's keys.""" + keys = list(self.non_gui_children[self.current_enum].keys()) + keys.insert(0, [self.current_enum]) + return keys + + def values(self): + """Children entities.""" + values = [ + self.enum_entity + ] + for child_entiy in self.non_gui_children[self.current_enum].values(): + values.append(child_entiy) + return values + + def items(self): + """Children entities paired with their key (key, value).""" + items = [ + (self.enum_key, self.enum_entity) + ] + for key, value in self.non_gui_children[self.current_enum].items(): + items.append((key, value)) + return items + def _item_initalization(self): self._default_metadata = NOT_SET self._studio_override_metadata = NOT_SET From 9ccc667c85471437c5acace23f2279b13e97380a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:53:45 +0200 Subject: [PATCH 226/359] implemented idea of set value --- openpype/settings/entities/dict_conditional.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 71e727e53f..a933dfd586 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -124,6 +124,16 @@ class DictConditionalEntity(ItemEntity): items.append((key, value)) return items + def set(self, value): + """Set value.""" + new_value = self.convert_to_valid_type(value) + # First change value of enum key if available + if self.enum_key in new_value: + self.enum_entity.set(new_value.pop(self.enum_key)) + + for _key, _value in new_value.items(): + self.non_gui_children[self.current_enum][_key].set(_value) + def _item_initalization(self): self._default_metadata = NOT_SET self._studio_override_metadata = NOT_SET From 469dcc09fa7051313039b81e7ff4a6ebcdf840aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:55:34 +0200 Subject: [PATCH 227/359] implemented change callbacks --- .../settings/entities/dict_conditional.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index a933dfd586..9e6b5b6f36 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -227,6 +227,25 @@ class DictConditionalEntity(ItemEntity): for child_obj in children: child_obj.schema_validations() + def on_change(self): + """Update metadata on change and pass change to parent.""" + self._update_current_metadata() + + for callback in self.on_change_callbacks: + callback() + self.parent.on_child_change(self) + + def on_child_change(self, child_obj): + """Trigger on change callback if child changes are not ignored.""" + if self._ignore_child_changes: + return + + if ( + child_obj is self.enum_entity + or child_obj in self.children[self.current_enum] + ): + self.on_change() + def _add_children(self): """Add children from schema data and repare enum items. From be1f0f77a05351bd7c3a154ad35e787a392701d3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 17:58:55 +0200 Subject: [PATCH 228/359] implemented set_override_state --- openpype/settings/entities/dict_conditional.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 9e6b5b6f36..8bf9b87218 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -322,3 +322,20 @@ class DictConditionalEntity(ItemEntity): raise ValueError("Didn't found child {}".format(child_obj)) return "/".join([self.path, result_key]) + + def set_override_state(self, state): + # Trigger override state change of root if is not same + if self.root_item.override_state is not state: + self.root_item.set_override_state(state) + return + + # Change has/had override states + self._override_state = state + + self.enum_entity.set_override_state(state) + + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.set_override_state(state) + + self._update_current_metadata() From 92d0c9f37b3e116700ff25a3422acfb3436aefe6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:02:05 +0200 Subject: [PATCH 229/359] implemented `value` and `settings_value` --- .../settings/entities/dict_conditional.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8bf9b87218..d2bab1ed15 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -339,3 +339,53 @@ class DictConditionalEntity(ItemEntity): child_obj.set_override_state(state) self._update_current_metadata() + + @property + def value(self): + output = { + self.current_enum: self.enum_entity.value + } + for key, child_obj in self.non_gui_children[self.current_enum].items(): + output[key] = child_obj.value + return output + + def settings_value(self): + if self._override_state is OverrideState.NOT_DEFINED: + return NOT_SET + + if self._override_state is OverrideState.DEFAULTS: + output = { + self.current_enum: self.enum_entity.settings_value() + } + non_gui_children = self.non_gui_children[self.current_enum] + for key, child_obj in non_gui_children.items(): + child_value = child_obj.settings_value() + if not child_obj.is_file and not child_obj.file_item: + for _key, _value in child_value.items(): + new_key = "/".join([key, _key]) + output[new_key] = _value + else: + output[key] = child_value + return output + + if self.is_group: + if self._override_state is OverrideState.STUDIO: + if not self.has_studio_override: + return NOT_SET + elif self._override_state is OverrideState.PROJECT: + if not self.has_project_override: + return NOT_SET + + output = { + self.current_enum: self.enum_entity.settings_value() + } + for key, child_obj in self.non_gui_children[self.current_enum].items(): + value = child_obj.settings_value() + if value is not NOT_SET: + output[key] = value + + if not output: + return NOT_SET + + output.update(self._current_metadata) + return output From 04207c689fe584c8c2393ad95893f67d6e5dc4b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:02:23 +0200 Subject: [PATCH 230/359] implemented modification and override properties --- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d2bab1ed15..f82cc02e3e 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -349,6 +349,52 @@ class DictConditionalEntity(ItemEntity): output[key] = child_obj.value return output + @property + def has_unsaved_changes(self): + if self._metadata_are_modified: + return True + + return self._child_has_unsaved_changes + + @property + def _child_has_unsaved_changes(self): + if self.enum_entity.has_unsaved_changes: + return True + for child_obj in self.non_gui_children[self.current_enum].values(): + if child_obj.has_unsaved_changes: + return True + return False + + @property + def has_studio_override(self): + return self._child_has_studio_override + + @property + def _child_has_studio_override(self): + if self._override_state >= OverrideState.STUDIO: + if self.enum_entity.has_studio_override: + return True + + for child_obj in self.non_gui_children[self.current_enum].values(): + if child_obj.has_studio_override: + return True + return False + + @property + def has_project_override(self): + return self._child_has_project_override + + @property + def _child_has_project_override(self): + if self._override_state >= OverrideState.PROJECT: + if self.enum_entity.has_project_override: + return True + + for child_obj in self.non_gui_children[self.current_enum].values(): + if child_obj.has_project_override: + return True + return False + def settings_value(self): if self._override_state is OverrideState.NOT_DEFINED: return NOT_SET From 58ade824c5a49f0f59fcd6ad0d46c24ded5bd7c1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:02:45 +0200 Subject: [PATCH 231/359] implemented update current metadata --- .../settings/entities/dict_conditional.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index f82cc02e3e..df7699a90e 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -323,6 +323,45 @@ class DictConditionalEntity(ItemEntity): return "/".join([self.path, result_key]) + def _update_current_metadata(self): + current_metadata = {} + for key, child_obj in self.non_gui_children[self.current_enum].items(): + if self._override_state is OverrideState.DEFAULTS: + break + + if not child_obj.is_group: + continue + + if ( + self._override_state is OverrideState.STUDIO + and not child_obj.has_studio_override + ): + continue + + if ( + self._override_state is OverrideState.PROJECT + and not child_obj.has_project_override + ): + continue + + if M_OVERRIDEN_KEY not in current_metadata: + current_metadata[M_OVERRIDEN_KEY] = [] + current_metadata[M_OVERRIDEN_KEY].append(key) + + # Define if current metadata are avaialble for current override state + metadata = NOT_SET + if self._override_state is OverrideState.STUDIO: + metadata = self._studio_override_metadata + + elif self._override_state is OverrideState.PROJECT: + metadata = self._project_override_metadata + + if metadata is NOT_SET: + metadata = {} + + self._metadata_are_modified = current_metadata != metadata + self._current_metadata = current_metadata + def set_override_state(self, state): # Trigger override state change of root if is not same if self.root_item.override_state is not state: From 1f9ba64a45bbe6b2300436b341b10e985597ca63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:02:55 +0200 Subject: [PATCH 232/359] implemented prepare value --- .../settings/entities/dict_conditional.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index df7699a90e..8172550075 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -474,3 +474,37 @@ class DictConditionalEntity(ItemEntity): output.update(self._current_metadata) return output + + def _prepare_value(self, value): + if value is NOT_SET or self.enum_key not in value: + return NOT_SET, NOT_SET + + enum_value = value.get(self.enum_key) + if enum_value not in self.non_gui_children: + return NOT_SET, NOT_SET + + # Create copy of value before poping values + value = copy.deepcopy(value) + metadata = {} + for key in METADATA_KEYS: + if key in value: + metadata[key] = value.pop(key) + + enum_value = value.get(self.enum_key) + + old_metadata = metadata.get(M_OVERRIDEN_KEY) + if old_metadata: + old_metadata_set = set(old_metadata) + new_metadata = [] + non_gui_children = self.non_gui_children[enum_value] + for key in non_gui_children.keys(): + if key in old_metadata: + new_metadata.append(key) + old_metadata_set.remove(key) + + for key in old_metadata_set: + new_metadata.append(key) + metadata[M_OVERRIDEN_KEY] = new_metadata + + return value, metadata + From f47ec0df6cfe1137e78afa14b27c54b8e4f88e49 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:15:06 +0200 Subject: [PATCH 233/359] implemented update methods --- .../settings/entities/dict_conditional.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8172550075..20697506fd 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -508,3 +508,112 @@ class DictConditionalEntity(ItemEntity): return value, metadata + def update_default_value(self, value): + """Update default values. + + Not an api method, should be called by parent. + """ + value = self._check_update_value(value, "default") + self.has_default_value = value is not NOT_SET + # TODO add value validation + value, metadata = self._prepare_value(value) + self._default_metadata = metadata + + if value is NOT_SET: + self.enum_entity.update_default_value(value) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key: + child_obj.update_default_value(value) + return + + value_keys = set(value.keys()) + enum_value = value[self.enum_key] + expected_keys = set(self.non_gui_children[enum_value]) + expected_keys.add(self.enum_key) + unknown_keys = value_keys - expected_keys + if unknown_keys: + self.log.warning( + "{} Unknown keys in default values: {}".format( + self.path, + ", ".join("\"{}\"".format(key) for key in unknown_keys) + ) + ) + + self.enum_entity.update_default_value(enum_value) + for children_by_key in self.non_gui_children.items(): + for key, child_obj in children_by_key.items(): + child_value = value.get(key, NOT_SET) + child_obj.update_default_value(child_value) + + def update_studio_value(self, value): + """Update studio override values. + + Not an api method, should be called by parent. + """ + value = self._check_update_value(value, "studio override") + value, metadata = self._prepare_value(value) + self._studio_override_metadata = metadata + self.had_studio_override = metadata is not NOT_SET + + if value is NOT_SET: + self.enum_entity.update_default_value(value) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key: + child_obj.update_default_value(value) + return + + value_keys = set(value.keys()) + enum_value = value[self.enum_key] + expected_keys = set(self.non_gui_children[enum_value]) + expected_keys.add(self.enum_key) + unknown_keys = value_keys - expected_keys + if unknown_keys: + self.log.warning( + "{} Unknown keys in studio overrides: {}".format( + self.path, + ", ".join("\"{}\"".format(key) for key in unknown_keys) + ) + ) + + self.enum_entity.update_studio_value(enum_value) + for children_by_key in self.non_gui_children.items(): + for key, child_obj in children_by_key.items(): + child_value = value.get(key, NOT_SET) + child_obj.update_studio_value(child_value) + + def update_project_value(self, value): + """Update project override values. + + Not an api method, should be called by parent. + """ + value = self._check_update_value(value, "project override") + value, metadata = self._prepare_value(value) + self._project_override_metadata = metadata + self.had_project_override = metadata is not NOT_SET + + if value is NOT_SET: + self.enum_entity.update_default_value(value) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key: + child_obj.update_default_value(value) + return + + value_keys = set(value.keys()) + enum_value = value[self.enum_key] + expected_keys = set(self.non_gui_children[enum_value]) + expected_keys.add(self.enum_key) + unknown_keys = value_keys - expected_keys + if unknown_keys: + self.log.warning( + "{} Unknown keys in project overrides: {}".format( + self.path, + ", ".join("\"{}\"".format(key) for key in unknown_keys) + ) + ) + + self.enum_entity.update_project_value(enum_value) + for children_by_key in self.non_gui_children.items(): + for key, child_obj in children_by_key.items(): + child_value = value.get(key, NOT_SET) + child_obj.update_project_value(child_value) + From baf706627af1cfc6a4724897d3ddfda054bcac37 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:15:28 +0200 Subject: [PATCH 234/359] implemented actions for conditional dictionary --- .../settings/entities/dict_conditional.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 20697506fd..5549ce13f2 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -617,3 +617,64 @@ class DictConditionalEntity(ItemEntity): child_value = value.get(key, NOT_SET) child_obj.update_project_value(child_value) + def _discard_changes(self, on_change_trigger): + self._ignore_child_changes = True + + self.enum_entity.discard_changes(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.discard_changes(on_change_trigger) + + self._ignore_child_changes = False + + def _add_to_studio_default(self, on_change_trigger): + self._ignore_child_changes = True + + self.enum_entity.add_to_studio_default(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.add_to_studio_default(on_change_trigger) + + self._ignore_child_changes = False + + self._update_current_metadata() + + self.parent.on_child_change(self) + + def _remove_from_studio_default(self, on_change_trigger): + self._ignore_child_changes = True + + self.enum_entity.remove_from_studio_default(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.remove_from_studio_default(on_change_trigger) + + self._ignore_child_changes = False + + def _add_to_project_override(self, on_change_trigger): + self._ignore_child_changes = True + + self.enum_entity.add_to_project_override(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.add_to_project_override(on_change_trigger) + + self._ignore_child_changes = False + + self._update_current_metadata() + + self.parent.on_child_change(self) + + def _remove_from_project_override(self, on_change_trigger): + if self._override_state is not OverrideState.PROJECT: + return + + self._ignore_child_changes = True + + self.enum_entity.remove_from_project_override(on_change_trigger) + for children_by_key in self.non_gui_children.values(): + for child_obj in children_by_key.values(): + child_obj.remove_from_project_override(on_change_trigger) + + self._ignore_child_changes = False + From f70b19305cd69f4d9755324b97dfc6702970b290 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:16:07 +0200 Subject: [PATCH 235/359] implemented reset_callbacks method --- openpype/settings/entities/dict_conditional.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 5549ce13f2..df852587f6 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -678,3 +678,9 @@ class DictConditionalEntity(ItemEntity): self._ignore_child_changes = False + def reset_callbacks(self): + """Reset registered callbacks on entity and children.""" + super(DictConditionalEntity, self).reset_callbacks() + for children in self.children.values(): + for child_entity in children: + child_entity.reset_callbacks() From 1048a1268cde84d2d0236c78d6eacc7d5f2a25d1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:18:02 +0200 Subject: [PATCH 236/359] import DictConditionalEntity in entities init --- openpype/settings/entities/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 94eb819f2b..c0eef15e69 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -111,6 +111,7 @@ from .enum_entity import ( from .list_entity import ListEntity from .dict_immutable_keys_entity import DictImmutableKeysEntity from .dict_mutable_keys_entity import DictMutableKeysEntity +from .dict_conditional import DictConditionalEntity from .anatomy_entities import AnatomyEntity @@ -166,5 +167,7 @@ __all__ = ( "DictMutableKeysEntity", + "DictConditionalEntity", + "AnatomyEntity" ) From e8f7f1418e6a11c510825f7337388b2909b77214 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:32:35 +0200 Subject: [PATCH 237/359] fix example schema --- openpype/settings/entities/dict_conditional.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index df852587f6..e5803b7606 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -57,6 +57,8 @@ example_schema = { "label": "Menu", "children": [ { + "key": "children", + "label": "Children", "type": "list", "object_type": "text" } From 85a3dd1ea6d6742df6eb23d4a8609db3a892c8d4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 18:32:54 +0200 Subject: [PATCH 238/359] fix dict vs. list approach --- openpype/settings/entities/dict_conditional.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index e5803b7606..8fc22348a7 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -524,7 +524,7 @@ class DictConditionalEntity(ItemEntity): if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): - for child_obj in children_by_key: + for child_obj in children_by_key.values(): child_obj.update_default_value(value) return @@ -560,7 +560,7 @@ class DictConditionalEntity(ItemEntity): if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): - for child_obj in children_by_key: + for child_obj in children_by_key.values(): child_obj.update_default_value(value) return @@ -596,7 +596,7 @@ class DictConditionalEntity(ItemEntity): if value is NOT_SET: self.enum_entity.update_default_value(value) for children_by_key in self.non_gui_children.values(): - for child_obj in children_by_key: + for child_obj in children_by_key.values(): child_obj.update_default_value(value) return From 16ac770359d5d9d78e9db11fde41649838e922e1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 19:57:42 +0200 Subject: [PATCH 239/359] created copy of DictConditionalWidget as base for DictConditionalWidget --- .../tools/settings/settings/categories.py | 5 + .../settings/settings/dict_conditional.py | 253 ++++++++++++++++++ 2 files changed, 258 insertions(+) create mode 100644 openpype/tools/settings/settings/dict_conditional.py diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 34ab4c464a..0dfafce186 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -11,6 +11,7 @@ from openpype.settings.entities import ( GUIEntity, DictImmutableKeysEntity, DictMutableKeysEntity, + DictConditionalEntity, ListEntity, PathEntity, ListStrictEntity, @@ -35,6 +36,7 @@ from .base import GUIWidget from .list_item_widget import ListWidget from .list_strict_widget import ListStrictWidget from .dict_mutable_widget import DictMutableKeysWidget +from .dict_conditional import DictConditionalWidget from .item_widgets import ( BoolWidget, DictImmutableKeysWidget, @@ -100,6 +102,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): if isinstance(entity, GUIEntity): return GUIWidget(*args) + elif isinstance(entity, DictConditionalEntity): + return DictConditionalWidget(*args) + elif isinstance(entity, DictImmutableKeysEntity): return DictImmutableKeysWidget(*args) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py new file mode 100644 index 0000000000..e7e0a31401 --- /dev/null +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -0,0 +1,253 @@ +import collections +from Qt import QtWidgets, QtCore, QtGui + +from .widgets import ( + ExpandingWidget, + GridLabelWidget +) +from .wrapper_widgets import ( + WrapperWidget, + CollapsibleWrapper, + FormWrapper +) +from .base import BaseWidget +from openpype.tools.settings import CHILD_OFFSET + + +class DictConditionalWidget(BaseWidget): + def create_ui(self): + self.input_fields = [] + self.checkbox_child = None + + self.label_widget = None + self.body_widget = None + self.content_widget = None + self.content_layout = None + + label = None + if self.entity.is_dynamic_item: + self._ui_as_dynamic_item() + + elif self.entity.use_label_wrap: + self._ui_label_wrap() + self.checkbox_child = self.entity.non_gui_children.get( + self.entity.checkbox_key + ) + + else: + self._ui_item_base() + label = self.entity.label + + self._parent_widget_by_entity_id = {} + self._added_wrapper_ids = set() + self._prepare_entity_layouts( + self.entity.gui_layout, self.content_widget + ) + + for child_obj in self.entity.children: + self.input_fields.append( + self.create_ui_for_entity( + self.category_widget, child_obj, self + ) + ) + + if self.entity.use_label_wrap and self.content_layout.count() == 0: + self.body_widget.hide_toolbox(True) + + self.entity_widget.add_widget_to_layout(self, label) + + def _prepare_entity_layouts(self, children, widget): + for child in children: + if not isinstance(child, dict): + if child is not self.checkbox_child: + self._parent_widget_by_entity_id[child.id] = widget + continue + + if child["type"] == "collapsible-wrap": + wrapper = CollapsibleWrapper(child, widget) + + elif child["type"] == "form": + wrapper = FormWrapper(child, widget) + + else: + raise KeyError( + "Unknown Wrapper type \"{}\"".format(child["type"]) + ) + + self._parent_widget_by_entity_id[wrapper.id] = widget + + self._prepare_entity_layouts(child["children"], wrapper) + + def _ui_item_base(self): + self.setObjectName("DictInvisible") + + self.content_widget = self + self.content_layout = QtWidgets.QGridLayout(self) + self.content_layout.setContentsMargins(0, 0, 0, 0) + self.content_layout.setSpacing(5) + + def _ui_as_dynamic_item(self): + content_widget = QtWidgets.QWidget(self) + content_widget.setObjectName("DictAsWidgetBody") + + show_borders = str(int(self.entity.show_borders)) + content_widget.setProperty("show_borders", show_borders) + + label_widget = QtWidgets.QLabel(self.entity.label) + + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setContentsMargins(5, 5, 5, 5) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(5) + main_layout.addWidget(content_widget) + + self.label_widget = label_widget + self.content_widget = content_widget + self.content_layout = content_layout + + def _ui_label_wrap(self): + content_widget = QtWidgets.QWidget(self) + content_widget.setObjectName("ContentWidget") + + if self.entity.highlight_content: + content_state = "hightlighted" + bottom_margin = 5 + else: + content_state = "" + bottom_margin = 0 + content_widget.setProperty("content_state", content_state) + content_layout_margins = (CHILD_OFFSET, 5, 0, bottom_margin) + + body_widget = ExpandingWidget(self.entity.label, self) + label_widget = body_widget.label_widget + body_widget.set_content_widget(content_widget) + + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setContentsMargins(*content_layout_margins) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(body_widget) + + self.label_widget = label_widget + self.body_widget = body_widget + self.content_widget = content_widget + self.content_layout = content_layout + + if len(self.input_fields) == 1 and self.checkbox_child: + body_widget.hide_toolbox(hide_content=True) + + elif self.entity.collapsible: + if not self.entity.collapsed: + body_widget.toggle_content() + else: + body_widget.hide_toolbox(hide_content=False) + + def add_widget_to_layout(self, widget, label=None): + if self.checkbox_child and widget.entity is self.checkbox_child: + self.body_widget.add_widget_before_label(widget) + return + + if not widget.entity: + map_id = widget.id + else: + map_id = widget.entity.id + + wrapper = self._parent_widget_by_entity_id[map_id] + if wrapper is not self.content_widget: + wrapper.add_widget_to_layout(widget, label) + if wrapper.id not in self._added_wrapper_ids: + self.add_widget_to_layout(wrapper) + self._added_wrapper_ids.add(wrapper.id) + return + + row = self.content_layout.rowCount() + if not label or isinstance(widget, WrapperWidget): + self.content_layout.addWidget(widget, row, 0, 1, 2) + else: + label_widget = GridLabelWidget(label, widget) + label_widget.input_field = widget + widget.label_widget = label_widget + self.content_layout.addWidget(label_widget, row, 0, 1, 1) + self.content_layout.addWidget(widget, row, 1, 1, 1) + + def set_entity_value(self): + for input_field in self.input_fields: + input_field.set_entity_value() + + def hierarchical_style_update(self): + self.update_style() + for input_field in self.input_fields: + input_field.hierarchical_style_update() + + def update_style(self): + if not self.body_widget and not self.label_widget: + return + + if self.entity.group_item: + group_item = self.entity.group_item + has_unsaved_changes = group_item.has_unsaved_changes + has_project_override = group_item.has_project_override + has_studio_override = group_item.has_studio_override + else: + has_unsaved_changes = self.entity.has_unsaved_changes + has_project_override = self.entity.has_project_override + has_studio_override = self.entity.has_studio_override + + style_state = self.get_style_state( + self.is_invalid, + has_unsaved_changes, + has_project_override, + has_studio_override + ) + if self._style_state == style_state: + return + + self._style_state = style_state + + if self.body_widget: + if style_state: + child_style_state = "child-{}".format(style_state) + else: + child_style_state = "" + + self.body_widget.side_line_widget.setProperty( + "state", child_style_state + ) + self.body_widget.side_line_widget.style().polish( + self.body_widget.side_line_widget + ) + + # There is nothing to care if there is no label + if not self.label_widget: + return + + # Don't change label if is not group or under group item + if not self.entity.is_group and not self.entity.group_item: + return + + self.label_widget.setProperty("state", style_state) + self.label_widget.style().polish(self.label_widget) + + def _on_entity_change(self): + pass + + @property + def is_invalid(self): + return self._is_invalid or self._child_invalid + + @property + def _child_invalid(self): + for input_field in self.input_fields: + if input_field.is_invalid: + return True + return False + + def get_invalid(self): + invalid = [] + for input_field in self.input_fields: + invalid.extend(input_field.get_invalid()) + return invalid From c3614dbfce046c692968399a9e7433613bff6655 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 19:58:29 +0200 Subject: [PATCH 240/359] removed checkbox checks as checkbox is not available for conditional dict --- .../tools/settings/settings/dict_conditional.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index e7e0a31401..2024bd3258 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -17,7 +17,6 @@ from openpype.tools.settings import CHILD_OFFSET class DictConditionalWidget(BaseWidget): def create_ui(self): self.input_fields = [] - self.checkbox_child = None self.label_widget = None self.body_widget = None @@ -30,9 +29,6 @@ class DictConditionalWidget(BaseWidget): elif self.entity.use_label_wrap: self._ui_label_wrap() - self.checkbox_child = self.entity.non_gui_children.get( - self.entity.checkbox_key - ) else: self._ui_item_base() @@ -59,8 +55,7 @@ class DictConditionalWidget(BaseWidget): def _prepare_entity_layouts(self, children, widget): for child in children: if not isinstance(child, dict): - if child is not self.checkbox_child: - self._parent_widget_by_entity_id[child.id] = widget + parent_widget_by_entity_id[child.id] = widget continue if child["type"] == "collapsible-wrap": @@ -137,20 +132,13 @@ class DictConditionalWidget(BaseWidget): self.content_widget = content_widget self.content_layout = content_layout - if len(self.input_fields) == 1 and self.checkbox_child: - body_widget.hide_toolbox(hide_content=True) - - elif self.entity.collapsible: + if self.entity.collapsible: if not self.entity.collapsed: body_widget.toggle_content() else: body_widget.hide_toolbox(hide_content=False) def add_widget_to_layout(self, widget, label=None): - if self.checkbox_child and widget.entity is self.checkbox_child: - self.body_widget.add_widget_before_label(widget) - return - if not widget.entity: map_id = widget.id else: From 1b1ce1f2a5602b07714f19e8aeb0404ba072f7f7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 19:59:07 +0200 Subject: [PATCH 241/359] modified _prepare_entity_layouts to be able to store result into passed dictionary --- openpype/tools/settings/settings/dict_conditional.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 2024bd3258..aeb2b7d86c 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -52,8 +52,10 @@ class DictConditionalWidget(BaseWidget): self.entity_widget.add_widget_to_layout(self, label) - def _prepare_entity_layouts(self, children, widget): - for child in children: + def _prepare_entity_layouts( + self, gui_layout, widget, parent_widget_by_entity_id + ): + for child in gui_layout: if not isinstance(child, dict): parent_widget_by_entity_id[child.id] = widget continue @@ -69,9 +71,11 @@ class DictConditionalWidget(BaseWidget): "Unknown Wrapper type \"{}\"".format(child["type"]) ) - self._parent_widget_by_entity_id[wrapper.id] = widget + parent_widget_by_entity_id[wrapper.id] = widget - self._prepare_entity_layouts(child["children"], wrapper) + self._prepare_entity_layouts( + child["children"], wrapper, parent_widget_by_entity_id + ) def _ui_item_base(self): self.setObjectName("DictInvisible") From 9131982be41add0330eaef9f0812e056eb194c13 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 19:59:37 +0200 Subject: [PATCH 242/359] added few required attributes --- openpype/tools/settings/settings/dict_conditional.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index aeb2b7d86c..47c1d7d4c9 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -18,6 +18,9 @@ class DictConditionalWidget(BaseWidget): def create_ui(self): self.input_fields = [] + self._content_by_enum_value = {} + self._last_enum_value = None + self.label_widget = None self.body_widget = None self.content_widget = None @@ -35,6 +38,7 @@ class DictConditionalWidget(BaseWidget): label = self.entity.label self._parent_widget_by_entity_id = {} + self._enum_key_by_wrapper_id = {} self._added_wrapper_ids = set() self._prepare_entity_layouts( self.entity.gui_layout, self.content_widget From 84f725b36447aca06ed676a2df3b44ede503dc8d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:02:02 +0200 Subject: [PATCH 243/359] modified how preparation of layout works --- .../settings/settings/dict_conditional.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 47c1d7d4c9..2287e52595 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -40,9 +40,22 @@ class DictConditionalWidget(BaseWidget): self._parent_widget_by_entity_id = {} self._enum_key_by_wrapper_id = {} self._added_wrapper_ids = set() - self._prepare_entity_layouts( - self.entity.gui_layout, self.content_widget - ) + + # Add enum entity to layout mapping + enum_entity = self.entity.enum_entity + self._parent_widget_by_entity_id[enum_entity.id] = self.content_widget + + # Add rest of entities to wrapper mappings + for enum_key, children in self.entity.gui_layout.items(): + parent_widget_by_entity_id = {} + self._prepare_entity_layouts( + children, + self.content_widget, + parent_widget_by_entity_id + ) + for item_id in parent_widget_by_entity_id.keys(): + self._enum_key_by_wrapper_id[item_id] = enum_key + self._parent_widget_by_entity_id.update(parent_widget_by_entity_id) for child_obj in self.entity.children: self.input_fields.append( From 2ebd5daac3be15bd3cc3feb68f863191f653df89 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:02:56 +0200 Subject: [PATCH 244/359] store content of each enum key to different widget --- .../tools/settings/settings/dict_conditional.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 2287e52595..c2e59e0fbe 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -48,9 +48,22 @@ class DictConditionalWidget(BaseWidget): # Add rest of entities to wrapper mappings for enum_key, children in self.entity.gui_layout.items(): parent_widget_by_entity_id = {} + + content_widget = QtWidgets.QWidget(self.content_widget) + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setColumnStretch(0, 0) + content_layout.setColumnStretch(1, 1) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(5) + + self._content_by_enum_value[enum_key] = { + "widget": content_widget, + "layout": content_layout + } + self._prepare_entity_layouts( children, - self.content_widget, + content_widget, parent_widget_by_entity_id ) for item_id in parent_widget_by_entity_id.keys(): From cc72287ddbccb400edea8f25d6ac1850f2359609 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:03:26 +0200 Subject: [PATCH 245/359] modified how entity widgets are created and when --- .../settings/settings/dict_conditional.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index c2e59e0fbe..3798cffe38 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -70,12 +70,23 @@ class DictConditionalWidget(BaseWidget): self._enum_key_by_wrapper_id[item_id] = enum_key self._parent_widget_by_entity_id.update(parent_widget_by_entity_id) - for child_obj in self.entity.children: - self.input_fields.append( - self.create_ui_for_entity( - self.category_widget, child_obj, self + enum_input_field = self.create_ui_for_entity( + self.category_widget, self.entity.enum_entity, self + ) + self.enum_input_field = enum_input_field + self.input_fields.append(enum_input_field) + + for item_key, children in self.entity.children.items(): + content_widget = self._content_by_enum_value[item_key]["widget"] + row = self.content_layout.rowCount() + self.content_layout.addWidget(content_widget, row, 0, 1, 2) + + for child_obj in children: + self.input_fields.append( + self.create_ui_for_entity( + self.category_widget, child_obj, self + ) ) - ) if self.entity.use_label_wrap and self.content_layout.count() == 0: self.body_widget.hide_toolbox(True) From c8a5de88bd5918a302132e0e37fa1bc362a41b7b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:03:52 +0200 Subject: [PATCH 246/359] define content widget based on map_id and entity id --- .../tools/settings/settings/dict_conditional.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 3798cffe38..013fefb74f 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -189,23 +189,30 @@ class DictConditionalWidget(BaseWidget): else: map_id = widget.entity.id + content_widget = self.content_widget + content_layout = self.content_layout + if map_id != self.entity.enum_entity.id: + enum_value = self._enum_key_by_wrapper_id[map_id] + content_widget = self._content_by_enum_value[enum_value]["widget"] + content_layout = self._content_by_enum_value[enum_value]["layout"] + wrapper = self._parent_widget_by_entity_id[map_id] - if wrapper is not self.content_widget: + if wrapper is not content_widget: wrapper.add_widget_to_layout(widget, label) if wrapper.id not in self._added_wrapper_ids: self.add_widget_to_layout(wrapper) self._added_wrapper_ids.add(wrapper.id) return - row = self.content_layout.rowCount() + row = content_layout.rowCount() if not label or isinstance(widget, WrapperWidget): - self.content_layout.addWidget(widget, row, 0, 1, 2) + content_layout.addWidget(widget, row, 0, 1, 2) else: label_widget = GridLabelWidget(label, widget) label_widget.input_field = widget widget.label_widget = label_widget - self.content_layout.addWidget(label_widget, row, 0, 1, 1) - self.content_layout.addWidget(widget, row, 1, 1, 1) + content_layout.addWidget(label_widget, row, 0, 1, 1) + content_layout.addWidget(widget, row, 1, 1, 1) def set_entity_value(self): for input_field in self.input_fields: From 313a78a3918cf279c0feceac64480b47afeb927e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:04:11 +0200 Subject: [PATCH 247/359] trigger change of visibility on change of enum --- openpype/tools/settings/settings/dict_conditional.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 013fefb74f..5442af14b4 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -273,7 +273,14 @@ class DictConditionalWidget(BaseWidget): self.label_widget.style().polish(self.label_widget) def _on_entity_change(self): - pass + enum_value = self.enum_input_field.entity.value + if enum_value == self._last_enum_value: + return + + self._last_enum_value = enum_value + for item_key, content in self._content_by_enum_value.items(): + widget = content["widget"] + widget.setVisible(item_key == enum_value) @property def is_invalid(self): From b344e0c27ad71350c887f2d90c3a2c5fe7ecb031 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:04:32 +0200 Subject: [PATCH 248/359] set_entity_value triggers on entity change --- openpype/tools/settings/settings/dict_conditional.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 5442af14b4..05dfa47e60 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -218,6 +218,8 @@ class DictConditionalWidget(BaseWidget): for input_field in self.input_fields: input_field.set_entity_value() + self._on_entity_change() + def hierarchical_style_update(self): self.update_style() for input_field in self.input_fields: From c9ee4e5f713f20dec0d4f684a596147b71f58850 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 28 Jun 2021 20:04:45 +0200 Subject: [PATCH 249/359] added column stretch to grid layout --- openpype/tools/settings/settings/dict_conditional.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 05dfa47e60..84288f7b5b 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -41,6 +41,9 @@ class DictConditionalWidget(BaseWidget): self._enum_key_by_wrapper_id = {} self._added_wrapper_ids = set() + self.content_layout.setColumnStretch(0, 0) + self.content_layout.setColumnStretch(1, 1) + # Add enum entity to layout mapping enum_entity = self.entity.enum_entity self._parent_widget_by_entity_id[enum_entity.id] = self.content_widget From 2f6f259c64da037574a7858f4ef3ec9efb35cb60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Jun 2021 20:51:56 +0000 Subject: [PATCH 250/359] Bump prismjs from 1.23.0 to 1.24.0 in /website Bumps [prismjs](https://github.com/PrismJS/prism) from 1.23.0 to 1.24.0. - [Release notes](https://github.com/PrismJS/prism/releases) - [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md) - [Commits](https://github.com/PrismJS/prism/compare/v1.23.0...v1.24.0) --- updated-dependencies: - dependency-name: prismjs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- website/yarn.lock | 39 +++------------------------------------ 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/website/yarn.lock b/website/yarn.lock index 2d5ec103d4..a63bf37731 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2667,15 +2667,6 @@ cli-boxes@^2.2.1: resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== -clipboard@^2.0.0: - version "2.0.8" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba" - integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -3310,11 +3301,6 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -4224,13 +4210,6 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= - dependencies: - delegate "^3.1.2" - got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -6615,11 +6594,9 @@ prism-react-renderer@^1.1.1: integrity sha512-GHqzxLYImx1iKN1jJURcuRoA/0ygCcNhfGw1IT8nPIMzarmKQ3Nc+JcG0gi8JXQzuh0C5ShE4npMIoqNin40hg== prismjs@^1.23.0: - version "1.23.0" - resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.23.0.tgz#d3b3967f7d72440690497652a9d40ff046067f33" - integrity sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA== - optionalDependencies: - clipboard "^2.0.0" + version "1.24.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.24.0.tgz#0409c30068a6c52c89ef7f1089b3ca4de56be2ac" + integrity sha512-SqV5GRsNqnzCL8k5dfAjCNhUrF3pR0A9lTDSCUZeh/LIshheXJEaP0hwLz2t4XHivd2J/v2HR+gRnigzeKe3cQ== process-nextick-args@~2.0.0: version "2.0.1" @@ -7390,11 +7367,6 @@ select-hose@^2.0.0: resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= - selfsigned@^1.10.8: version "1.10.8" resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.8.tgz#0d17208b7d12c33f8eac85c41835f27fc3d81a30" @@ -8016,11 +7988,6 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tiny-invariant@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" From e1129bdbad5a167e4428bd3d298e1e4e5012fdd9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:25:45 +0200 Subject: [PATCH 251/359] fix keys method --- openpype/settings/entities/dict_conditional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 8fc22348a7..f72d1c8b82 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -105,7 +105,7 @@ class DictConditionalEntity(ItemEntity): def keys(self): """Entity's keys.""" keys = list(self.non_gui_children[self.current_enum].keys()) - keys.insert(0, [self.current_enum]) + keys.insert(0, [self.enum_key]) return keys def values(self): From 4bff4a8138506f1fdbe247e54d4fb739bb111bff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:29:07 +0200 Subject: [PATCH 252/359] force to be a group --- openpype/settings/entities/dict_conditional.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index f72d1c8b82..2e8cd6affe 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -149,6 +149,13 @@ class DictConditionalEntity(ItemEntity): self._current_metadata = {} self._metadata_are_modified = False + if ( + self.group_item is None + and not self.is_dynamic_item + and not self.is_in_dynamic_item + ): + self.is_group = True + # Children are stored by key as keys are immutable and are defined by # schema self.valid_value_types = (dict, ) From 40f87f2f20c5fee45f2be2e903aaf9814f9acf43 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:30:11 +0200 Subject: [PATCH 253/359] few minor fixes of entity --- .../settings/entities/dict_conditional.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 2e8cd6affe..d3aad60df6 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -391,7 +391,7 @@ class DictConditionalEntity(ItemEntity): @property def value(self): output = { - self.current_enum: self.enum_entity.value + self.enum_key: self.enum_entity.value } for key, child_obj in self.non_gui_children[self.current_enum].items(): output[key] = child_obj.value @@ -408,6 +408,7 @@ class DictConditionalEntity(ItemEntity): def _child_has_unsaved_changes(self): if self.enum_entity.has_unsaved_changes: return True + for child_obj in self.non_gui_children[self.current_enum].values(): if child_obj.has_unsaved_changes: return True @@ -448,11 +449,14 @@ class DictConditionalEntity(ItemEntity): return NOT_SET if self._override_state is OverrideState.DEFAULTS: - output = { - self.current_enum: self.enum_entity.settings_value() - } - non_gui_children = self.non_gui_children[self.current_enum] - for key, child_obj in non_gui_children.items(): + children_items = [ + (self.enum_key, self.enum_entity) + ] + for item in self.non_gui_children[self.current_enum].items(): + children_items.append(item) + + output = {} + for key, child_obj in children_items: child_value = child_obj.settings_value() if not child_obj.is_file and not child_obj.file_item: for _key, _value in child_value.items(): @@ -470,10 +474,14 @@ class DictConditionalEntity(ItemEntity): if not self.has_project_override: return NOT_SET - output = { - self.current_enum: self.enum_entity.settings_value() - } - for key, child_obj in self.non_gui_children[self.current_enum].items(): + output = {} + children_items = [ + (self.enum_key, self.enum_entity) + ] + for item in self.non_gui_children[self.current_enum].items(): + children_items.append(item) + + for key, child_obj in children_items: value = child_obj.settings_value() if value is not NOT_SET: output[key] = value @@ -537,7 +545,7 @@ class DictConditionalEntity(ItemEntity): value_keys = set(value.keys()) enum_value = value[self.enum_key] - expected_keys = set(self.non_gui_children[enum_value]) + expected_keys = set(self.non_gui_children[enum_value].keys()) expected_keys.add(self.enum_key) unknown_keys = value_keys - expected_keys if unknown_keys: @@ -549,7 +557,7 @@ class DictConditionalEntity(ItemEntity): ) self.enum_entity.update_default_value(enum_value) - for children_by_key in self.non_gui_children.items(): + for children_by_key in self.non_gui_children.values(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_default_value(child_value) @@ -565,10 +573,10 @@ class DictConditionalEntity(ItemEntity): self.had_studio_override = metadata is not NOT_SET if value is NOT_SET: - self.enum_entity.update_default_value(value) + self.enum_entity.update_studio_value(value) for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): - child_obj.update_default_value(value) + child_obj.update_studio_value(value) return value_keys = set(value.keys()) @@ -601,10 +609,10 @@ class DictConditionalEntity(ItemEntity): self.had_project_override = metadata is not NOT_SET if value is NOT_SET: - self.enum_entity.update_default_value(value) + self.enum_entity.update_project_value(value) for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): - child_obj.update_default_value(value) + child_obj.update_project_value(value) return value_keys = set(value.keys()) From 3d59ba17d54600247593a7c05c00a0576e124dc1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:37:04 +0200 Subject: [PATCH 254/359] current_enum is dynamic property --- openpype/settings/entities/dict_conditional.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d3aad60df6..6802af5806 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -171,10 +171,15 @@ class DictConditionalEntity(ItemEntity): self.enum_children = self.schema_data.get("enum_children") self.enum_entity = None - self.current_enum = None self._add_children() + @property + def current_enum(self): + if self.enum_entity is None: + return None + return self.enum_entity.value + def schema_validations(self): """Validation of schema data.""" if self.enum_key is None: @@ -269,25 +274,20 @@ class DictConditionalEntity(ItemEntity): if not self.enum_children or not self.enum_key: return - enum_items = [] valid_enum_items = [] for item in self.enum_children: if isinstance(item, dict) and "key" in item: valid_enum_items.append(item) - first_key = None + enum_items = [] for item in valid_enum_items: item_key = item["key"] - if first_key is None: - first_key = item_key item_label = item.get("label") or item_key enum_items.append({item_key: item_label}) if not enum_items: return - self.current_enum = first_key - enum_key = self.enum_key or "invalid" enum_schema = { "type": "enum", @@ -296,6 +296,7 @@ class DictConditionalEntity(ItemEntity): "key": enum_key, "label": self.enum_label or enum_key } + enum_entity = self.create_schema_object(enum_schema, self) self.enum_entity = enum_entity From f3ae791f5c0afc283a5a17490bb70bf362e4a1e4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 10:56:13 +0200 Subject: [PATCH 255/359] make sure all keys are available in all variables --- openpype/settings/entities/dict_conditional.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 6802af5806..956180c3da 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -159,9 +159,9 @@ class DictConditionalEntity(ItemEntity): # Children are stored by key as keys are immutable and are defined by # schema self.valid_value_types = (dict, ) - self.children = collections.defaultdict(list) - self.non_gui_children = collections.defaultdict(dict) - self.gui_layout = collections.defaultdict(list) + self.children = {} + self.non_gui_children = {} + self.gui_layout = {} if self.is_dynamic_item: self.require_key = False @@ -302,6 +302,11 @@ class DictConditionalEntity(ItemEntity): for item in valid_enum_items: item_key = item["key"] + # Make sure all keys have set value in there variables + self.non_gui_children[item_key] = {} + self.children[item_key] = [] + self.gui_layout[item_key] = [] + children = item.get("children") or [] for children_schema in children: child_obj = self.create_schema_object(children_schema, self) From 42fbc1f633ebec2e04a3243b07a9341e55672424 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 11:33:44 +0200 Subject: [PATCH 256/359] decode ffprobe output before logging --- openpype/lib/vendor_bin_utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 3b923cb608..a8c75c20da 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -89,8 +89,13 @@ def ffprobe_streams(path_to_file, logger=None): popen_stdout, popen_stderr = popen.communicate() if popen_stdout: - logger.debug("ffprobe stdout: {}".format(popen_stdout)) + logger.debug("FFprobe stdout:\n{}".format( + popen_stdout.decode("utf-8") + )) if popen_stderr: - logger.debug("ffprobe stderr: {}".format(popen_stderr)) + logger.warning("FFprobe stderr:\n{}".format( + popen_stderr.decode("utf-8") + )) + return json.loads(popen_stdout)["streams"] From 404a659b40411bcb174b114a10012d89b20ed222 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 11:48:47 +0200 Subject: [PATCH 257/359] find first stream with resolution when reading ffprobe streams --- openpype/plugins/publish/extract_review.py | 30 +++++++++++++++---- .../plugins/publish/extract_review_slate.py | 20 +++++++++++-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 42fb2a8f93..de54b554e3 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -975,11 +975,31 @@ 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 = ffprobe_streams( - full_input_path_single_file, self.log - )[0] - input_width = int(input_data["width"]) - input_height = int(input_data["height"]) + try: + streams = ffprobe_streams( + full_input_path_single_file, self.log + ) + except Exception: + raise AssertionError(( + "FFprobe couldn't read information about input file: \"{}\"" + ).format(full_input_path_single_file)) + + # Try to find first stream with defined 'width' and 'height' + # - this is to avoid order of streams where audio can be as first + # - there may be a better way (checking `codec_type`?) + input_width = None + input_height = None + for stream in streams: + if "width" in stream and "height" in stream: + input_width = int(stream["width"]) + input_height = int(stream["height"]) + break + + # Raise exception of any stream didn't define input resolution + if input_width is None: + raise AssertionError(( + "FFprobe couldn't read resolution from input file: \"{}\"" + ).format(full_input_path_single_file)) # NOTE Setting only one of `width` or `heigth` is not allowed # - settings value can't have None but has value of 0 diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index fb36a930fb..6908f044d1 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -26,9 +26,23 @@ class ExtractReviewSlate(openpype.api.Extractor): slate_path = inst_data.get("slateFrame") ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg") - slate_stream = openpype.lib.ffprobe_streams(slate_path, self.log)[0] - slate_width = slate_stream["width"] - slate_height = slate_stream["height"] + slate_streams = openpype.lib.ffprobe_streams(slate_path, self.log) + # Try to find first stream with defined 'width' and 'height' + # - this is to avoid order of streams where audio can be as first + # - there may be a better way (checking `codec_type`?)+ + slate_width = None + slate_height = None + for slate_stream in slate_streams: + if "width" in slate_stream and "height" in slate_stream: + slate_width = int(slate_stream["width"]) + slate_height = int(slate_stream["height"]) + break + + # Raise exception of any stream didn't define input resolution + if slate_width is None: + raise AssertionError(( + "FFprobe couldn't read resolution from input file: \"{}\"" + ).format(slate_path)) if "reviewToWidth" in inst_data: use_legacy_code = True From 6d571b2e17eaeeea65d301b56b317881e08e1ab8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 11:49:22 +0200 Subject: [PATCH 258/359] find first stream that is not an audio when defying profile and pix_fmt --- .../plugins/publish/extract_review_slate.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 6908f044d1..2b07d7db74 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -323,16 +323,29 @@ class ExtractReviewSlate(openpype.api.Extractor): ) return codec_args - codec_name = streams[0].get("codec_name") + # Try to find first stream that is not an audio + no_audio_stream = None + for stream in streams: + if stream.get("codec_type") != "audio": + no_audio_stream = stream + break + + if no_audio_stream is None: + self.log.warning(( + "Couldn't find stream that is not an audio from file \"{}\"" + ).format(full_input_path)) + return codec_args + + codec_name = no_audio_stream.get("codec_name") if codec_name: codec_args.append("-codec:v {}".format(codec_name)) - profile_name = streams[0].get("profile") + profile_name = no_audio_stream.get("profile") if profile_name: profile_name = profile_name.replace(" ", "_").lower() codec_args.append("-profile:v {}".format(profile_name)) - pix_fmt = streams[0].get("pix_fmt") + pix_fmt = no_audio_stream.get("pix_fmt") if pix_fmt: codec_args.append("-pix_fmt {}".format(pix_fmt)) return codec_args From 0e0e527741392bad6a67de9f3d2736521a149326 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 12:44:50 +0200 Subject: [PATCH 259/359] items vs. values fix --- openpype/settings/entities/dict_conditional.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 956180c3da..858c2ca4e8 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -599,7 +599,7 @@ class DictConditionalEntity(ItemEntity): ) self.enum_entity.update_studio_value(enum_value) - for children_by_key in self.non_gui_children.items(): + for children_by_key in self.non_gui_children.values(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_studio_value(child_value) @@ -635,7 +635,7 @@ class DictConditionalEntity(ItemEntity): ) self.enum_entity.update_project_value(enum_value) - for children_by_key in self.non_gui_children.items(): + for children_by_key in self.non_gui_children.values(): for key, child_obj in children_by_key.items(): child_value = value.get(key, NOT_SET) child_obj.update_project_value(child_value) From d58c8f1a112a0daaf2e3227794923d0262ee344f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 12:46:10 +0200 Subject: [PATCH 260/359] added argument ignore_missing_defaults to set_override_state method --- openpype/settings/entities/base_entity.py | 9 ++++++++- openpype/settings/entities/dict_conditional.py | 2 +- .../entities/dict_immutable_keys_entity.py | 4 ++-- .../entities/dict_mutable_keys_entity.py | 14 ++++++++++---- openpype/settings/entities/input_entities.py | 12 +++++++++--- openpype/settings/entities/item_entities.py | 18 ++++++++++++------ openpype/settings/entities/list_entity.py | 16 ++++++++++++---- openpype/settings/entities/root_entities.py | 7 +++++-- 8 files changed, 59 insertions(+), 23 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 0e29a35e1f..e1cd5134e7 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -280,7 +280,7 @@ class BaseItemEntity(BaseEntity): ) @abstractmethod - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): """Set override state and trigger it on children. Method discard all changes in hierarchy and use values, metadata @@ -290,8 +290,15 @@ class BaseItemEntity(BaseEntity): Should start on root entity and when triggered then must be called on all entities in hierarchy. + Argument `ignore_missing_defaults` should be used when entity has + children that are not saved or used all the time but override statu + must be changed and children must have any default value. + Args: state (OverrideState): State to which should be data changed. + ignore_missing_defaults (bool): Ignore missing default values. + Entity won't raise `DefaultsNotDefined` and + `StudioDefaultsNotDefined`. """ pass diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 858c2ca4e8..98aa10dacb 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -377,7 +377,7 @@ class DictConditionalEntity(ItemEntity): self._metadata_are_modified = current_metadata != metadata self._current_metadata = current_metadata - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index c965dc3b5a..2802290e68 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -258,7 +258,7 @@ class DictImmutableKeysEntity(ItemEntity): self._metadata_are_modified = current_metadata != metadata self._current_metadata = current_metadata - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -268,7 +268,7 @@ class DictImmutableKeysEntity(ItemEntity): self._override_state = state for child_obj in self.non_gui_children.values(): - child_obj.set_override_state(state) + child_obj.set_override_state(state, ignore_missing_defaults) self._update_current_metadata() diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index 3c2645e3e5..a5734e36b8 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -320,7 +320,7 @@ class DictMutableKeysEntity(EndpointEntity): def _metadata_for_current_state(self): return self._get_metadata_for_state(self._override_state) - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -331,11 +331,17 @@ class DictMutableKeysEntity(EndpointEntity): # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: - if not self.has_default_value: + if ( + not self.has_default_value + and not ignore_missing_defaults + ): raise DefaultsNotDefined(self) elif state > OverrideState.STUDIO: - if not self.had_studio_override: + if ( + not self.had_studio_override + and not ignore_missing_defaults + ): raise StudioDefaultsNotDefined(self) if state is OverrideState.STUDIO: @@ -426,7 +432,7 @@ class DictMutableKeysEntity(EndpointEntity): if label: children_label_by_id[child_entity.id] = label - child_entity.set_override_state(state) + child_entity.set_override_state(state, ignore_missing_defaults) self.children_label_by_id = children_label_by_id diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 295333eb60..9b41a26bdb 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -217,7 +217,7 @@ class InputEntity(EndpointEntity): return True return False - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -227,11 +227,17 @@ class InputEntity(EndpointEntity): # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: - if not self.has_default_value: + if ( + not self.has_default_value + and not ignore_missing_defaults + ): raise DefaultsNotDefined(self) elif state > OverrideState.STUDIO: - if not self.had_studio_override: + if ( + not self.had_studio_override + and not ignore_missing_defaults + ): raise StudioDefaultsNotDefined(self) if state is OverrideState.STUDIO: diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index 48336080b6..c52eab988f 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -150,14 +150,14 @@ class PathEntity(ItemEntity): def value(self): return self.child_obj.value - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) return self._override_state = state - self.child_obj.set_override_state(state) + self.child_obj.set_override_state(state, ignore_missing_defaults) def update_default_value(self, value): self.child_obj.update_default_value(value) @@ -344,7 +344,7 @@ class ListStrictEntity(ItemEntity): return True return False - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -354,15 +354,21 @@ class ListStrictEntity(ItemEntity): # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: - if not self.has_default_value: + if ( + not self.has_default_value + and not ignore_missing_defaults + ): raise DefaultsNotDefined(self) elif state > OverrideState.STUDIO: - if not self.had_studio_override: + if ( + not self.had_studio_override + and not ignore_missing_defaults + ): raise StudioDefaultsNotDefined(self) for child_entity in self.children: - child_entity.set_override_state(state) + child_entity.set_override_state(state, ignore_missing_defaults) self.initial_value = self.settings_value() diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index 4b3f7a2659..2225523792 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -205,7 +205,7 @@ class ListEntity(EndpointEntity): self._has_project_override = True self.on_change() - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults): # Trigger override state change of root if is not same if self.root_item.override_state is not state: self.root_item.set_override_state(state) @@ -219,11 +219,17 @@ class ListEntity(EndpointEntity): # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: - if not self.has_default_value: + if ( + not self.has_default_value + and not ignore_missing_defaults + ): raise DefaultsNotDefined(self) elif state > OverrideState.STUDIO: - if not self.had_studio_override: + if ( + not self.had_studio_override + and not ignore_missing_defaults + ): raise StudioDefaultsNotDefined(self) value = NOT_SET @@ -257,7 +263,9 @@ class ListEntity(EndpointEntity): child_obj.update_studio_value(item) for child_obj in self.children: - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, ignore_missing_defaults + ) self.initial_value = self.settings_value() diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index 1833535a07..b758e30cbe 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -218,7 +218,7 @@ class RootEntity(BaseItemEntity): schema_data, *args, **kwargs ) - def set_override_state(self, state): + def set_override_state(self, state, ignore_missing_defaults=None): """Set override state and trigger it on children. Method will discard all changes in hierarchy and use values, metadata @@ -227,9 +227,12 @@ class RootEntity(BaseItemEntity): Args: state (OverrideState): State to which should be data changed. """ + if not ignore_missing_defaults: + ignore_missing_defaults = False + self._override_state = state for child_obj in self.non_gui_children.values(): - child_obj.set_override_state(state) + child_obj.set_override_state(state, ignore_missing_defaults) def on_change(self): """Trigger callbacks on change.""" From 0ed3a2ee701f26fc9cdebe19ceefb225848ae38c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 12:47:12 +0200 Subject: [PATCH 261/359] Use ignore missing defaults in conditional dictionary children that are not using current enum value --- openpype/settings/entities/dict_conditional.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 98aa10dacb..112ef8bddc 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -386,11 +386,17 @@ class DictConditionalEntity(ItemEntity): # Change has/had override states self._override_state = state - self.enum_entity.set_override_state(state) + self.enum_entity.set_override_state(state, ignore_missing_defaults) + + for child_obj in self.non_gui_children[self.current_enum].values(): + child_obj.set_override_state(state, ignore_missing_defaults) + + for item_key, children_by_key in self.non_gui_children.items(): + if item_key == self.current_enum: + continue - for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): - child_obj.set_override_state(state) + child_obj.set_override_state(state, True) self._update_current_metadata() From c1d6db4356e7258970dbffb9152a82f408bab025 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 13:04:52 +0200 Subject: [PATCH 262/359] added few comments and docstring --- .../settings/entities/dict_conditional.py | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 112ef8bddc..6e28cbd591 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -73,6 +73,19 @@ example_schema = { class DictConditionalEntity(ItemEntity): + """Entity represents dictionay with only one persistent key definition. + + The persistent key is enumerator which define rest of children under + dictionary. There is not possibility of shared children. + + Entity's keys can't be removed or added. But they may change based on + the persistent key. If you're change value manually (key by key) make sure + you'll change value of the persistent key as first. It is recommended to + use `set` method which handle this for you. + + It is possible to use entity similar way as `dict` object. Returned values + are not real settings values but entities representing the value. + """ schema_types = ["dict-conditional"] _default_label_wrap = { "use_label_wrap": False, @@ -149,6 +162,7 @@ class DictConditionalEntity(ItemEntity): self._current_metadata = {} self._metadata_are_modified = False + # Entity must be group or in group if ( self.group_item is None and not self.is_dynamic_item @@ -176,15 +190,21 @@ class DictConditionalEntity(ItemEntity): @property def current_enum(self): + """Current value of enum entity. + + This value define what children are used. + """ if self.enum_entity is None: return None return self.enum_entity.value def schema_validations(self): """Validation of schema data.""" + # Enum key must be defined if self.enum_key is None: raise EntitySchemaError(self, "Key 'enum_key' is not set.") + # Validate type of enum children if not isinstance(self.enum_children, list): raise EntitySchemaError( self, "Key 'enum_children' must be a list. Got: {}".format( @@ -192,6 +212,7 @@ class DictConditionalEntity(ItemEntity): ) ) + # Without defined enum children entity has nothing to do if not self.enum_children: raise EntitySchemaError(self, ( "Key 'enum_children' have empty value. Entity can't work" @@ -219,6 +240,7 @@ class DictConditionalEntity(ItemEntity): raise SchemaDuplicatedKeys(self, key) children_def_keys.append(key) + # Validate key duplications per each enum item for children in self.children.values(): children_keys = set() children_keys.add(self.enum_key) @@ -230,6 +252,7 @@ class DictConditionalEntity(ItemEntity): else: raise SchemaDuplicatedKeys(self, child_entity.key) + # Validate all remaining keys with key regex for children_by_key in self.non_gui_children.values(): for key in children_by_key.keys(): if not KEY_REGEX.match(key): @@ -270,7 +293,8 @@ class DictConditionalEntity(ItemEntity): All children are stored by their enum item. """ - # Skip and wait for validation + # Skip if are not defined + # - schema validations should raise and exception if not self.enum_children or not self.enum_key: return @@ -288,6 +312,7 @@ class DictConditionalEntity(ItemEntity): if not enum_items: return + # Create Enum child first enum_key = self.enum_key or "invalid" enum_schema = { "type": "enum", @@ -300,9 +325,11 @@ class DictConditionalEntity(ItemEntity): enum_entity = self.create_schema_object(enum_schema, self) self.enum_entity = enum_entity + # Create children per each enum item for item in valid_enum_items: item_key = item["key"] - # Make sure all keys have set value in there variables + # Make sure all keys have set value in these variables + # - key 'children' is optional self.non_gui_children[item_key] = {} self.children[item_key] = [] self.gui_layout[item_key] = [] @@ -386,11 +413,15 @@ class DictConditionalEntity(ItemEntity): # Change has/had override states self._override_state = state + # Set override state on enum entity first self.enum_entity.set_override_state(state, ignore_missing_defaults) + # Set override state on other entities under current enum value for child_obj in self.non_gui_children[self.current_enum].values(): child_obj.set_override_state(state, ignore_missing_defaults) + # Set override state on other enum children + # - these must not raise exception about missing defaults for item_key, children_by_key in self.non_gui_children.items(): if item_key == self.current_enum: continue From 3b217c57a2c8b2069b4cf3e4f5a03acbe66e90ff Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 13:05:02 +0200 Subject: [PATCH 263/359] added enum key validation --- openpype/settings/entities/dict_conditional.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 6e28cbd591..96e6c518f3 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -252,6 +252,10 @@ class DictConditionalEntity(ItemEntity): else: raise SchemaDuplicatedKeys(self, child_entity.key) + # Enum key must match key regex + if not KEY_REGEX.match(self.enum_key): + raise InvalidKeySymbols(self.path, self.enum_key) + # Validate all remaining keys with key regex for children_by_key in self.non_gui_children.values(): for key in children_by_key.keys(): From 605a0454b2c69ce3e76a4df85eced6e0364ae47a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 13:05:20 +0200 Subject: [PATCH 264/359] include enum_key in builtin methods --- openpype/settings/entities/dict_conditional.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 96e6c518f3..9ba24cf0de 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -95,11 +95,16 @@ class DictConditionalEntity(ItemEntity): def __getitem__(self, key): """Return entity inder key.""" + if key == self.enum_key: + return self.enum_entity return self.non_gui_children[self.current_enum][key] def __setitem__(self, key, value): """Set value of item under key.""" - child_obj = self.non_gui_children[self.current_enum][key] + if key == self.enum_key: + child_obj = self.enum_entity + else: + child_obj = self.non_gui_children[self.current_enum][key] child_obj.set(value) def __iter__(self): @@ -109,10 +114,14 @@ class DictConditionalEntity(ItemEntity): def __contains__(self, key): """Check if key is available.""" + if key == self.enum_key: + return True return key in self.non_gui_children[self.current_enum] def get(self, key, default=None): """Safe entity getter by key.""" + if key == self.enum_key: + return self.enum_entity return self.non_gui_children[self.current_enum].get(key, default) def keys(self): From 13aec9cb572ece21c79548a9d7748cf3002cef3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 13:47:37 +0200 Subject: [PATCH 265/359] "use_python_2" is optional in application settings --- openpype/lib/applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 9866400928..1eac7ea776 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -179,7 +179,7 @@ class Application: if group.enabled: enabled = data.get("enabled", True) self.enabled = enabled - self.use_python_2 = data["use_python_2"] + self.use_python_2 = data.get("use_python_2", False) self.label = data.get("variant_label") or name self.full_name = "/".join((group.name, name)) From 7daf1d3d0b41b9525aca9bbda39cd75923dc8898 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:04:37 +0200 Subject: [PATCH 266/359] do replacement only if replacement is still string --- openpype/settings/entities/lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 05f4ea64f8..cf0da29978 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -145,7 +145,10 @@ def _fill_schema_template_data( # Only replace the key in string template = template.replace(replacement_string, value) - output = template.replace("__dbcb__", "{").replace("__decb__", "}") + if isinstance(template, STRING_TYPE): + output = template.replace("__dbcb__", "{").replace("__decb__", "}") + else: + output = template else: output = template From cc85f669b8d4b9737959295cb23ee75913a0bfbc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:04:59 +0200 Subject: [PATCH 267/359] removed use_python_2 from blender --- openpype/settings/defaults/system_settings/applications.json | 3 --- .../schemas/system_schema/host_settings/schema_blender.json | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 72cd010cf2..583597df32 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -807,7 +807,6 @@ "environment": {}, "variants": { "2-83": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blender Foundation\\Blender 2.83\\blender.exe" @@ -829,7 +828,6 @@ "environment": {} }, "2-90": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blender Foundation\\Blender 2.90\\blender.exe" @@ -851,7 +849,6 @@ "environment": {} }, "2-91": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Blender Foundation\\Blender 2.91\\blender.exe" diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json index 0a6c8ca035..27ead6e6da 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_blender.json @@ -30,7 +30,8 @@ "children": [ { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": ["use_python_2"] } ] } From 2fd7bc4c1305ac4e1968a54391418615749004cf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:05:37 +0200 Subject: [PATCH 268/359] template_host_variant have ability to modify skip_paths on template_host_variant_items --- .../host_settings/template_host_variant.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json index 33cde3d216..96a936c27b 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/template_host_variant.json @@ -1,4 +1,9 @@ [ + { + "__default_values__": { + "variant_skip_paths": null + } + }, { "type": "dict", "key": "{app_variant}", @@ -19,7 +24,8 @@ }, { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": "{variant_skip_paths}" } ] } From 5bcff7230e5b786e1a4989ab934759fcc8447e72 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:05:54 +0200 Subject: [PATCH 269/359] removed use_python_2 from harmony --- .../settings/defaults/system_settings/applications.json | 2 -- .../schemas/system_schema/host_settings/schema_harmony.json | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 583597df32..b7eece8a6a 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -888,7 +888,6 @@ "20": { "enabled": true, "variant_label": "20", - "use_python_2": false, "executables": { "windows": [], "darwin": [], @@ -904,7 +903,6 @@ "17": { "enabled": true, "variant_label": "17", - "use_python_2": false, "executables": { "windows": [], "darwin": [ diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json index 083885a53b..c122b8930b 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_harmony.json @@ -29,11 +29,13 @@ "template_data": [ { "app_variant_label": "20", - "app_variant": "20" + "app_variant": "20", + "variant_skip_paths": ["use_python_2"] }, { "app_variant_label": "17", - "app_variant": "17" + "app_variant": "17", + "variant_skip_paths": ["use_python_2"] } ] } From e98d1a99ef40f384b90ede51d0221b996b84f247 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:06:08 +0200 Subject: [PATCH 270/359] remove use_python_2 from tvpaint --- openpype/settings/defaults/system_settings/applications.json | 2 -- .../schemas/system_schema/host_settings/schema_tvpaint.json | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index b7eece8a6a..ed09ec2815 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -927,7 +927,6 @@ "environment": {}, "variants": { "animation_11-64bits": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\TVPaint Developpement\\TVPaint Animation 11 (64bits)\\TVPaint Animation 11 (64bits).exe" @@ -943,7 +942,6 @@ "environment": {} }, "animation_11-32bits": { - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files (x86)\\TVPaint Developpement\\TVPaint Animation 11 (32bits)\\TVPaint Animation 11 (32bits).exe" diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json index c39e6f7a30..ff57d767c4 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_tvpaint.json @@ -30,7 +30,8 @@ "children": [ { "type": "schema_template", - "name": "template_host_variant_items" + "name": "template_host_variant_items", + "skip_paths": ["use_python_2"] } ] } From a95d0ac3ffc3ef0167b6d5d32ca8e413798fedfd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:06:30 +0200 Subject: [PATCH 271/359] remove use_python_2 from photoshop --- .../settings/defaults/system_settings/applications.json | 2 -- .../system_schema/host_settings/schema_photoshop.json | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index ed09ec2815..d8c9e171fc 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -975,7 +975,6 @@ "2020": { "enabled": true, "variant_label": "2020", - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Adobe\\Adobe Photoshop 2020\\Photoshop.exe" @@ -993,7 +992,6 @@ "2021": { "enabled": true, "variant_label": "2021", - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Adobe\\Adobe Photoshop 2021\\Photoshop.exe" diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json index 9c21166b63..7bcd89c650 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_photoshop.json @@ -29,11 +29,13 @@ "template_data": [ { "app_variant_label": "2020", - "app_variant": "2020" + "app_variant": "2020", + "variant_skip_paths": ["use_python_2"] }, { "app_variant_label": "2021", - "app_variant": "2021" + "app_variant": "2021", + "variant_skip_paths": ["use_python_2"] } ] } From 5b60e7f172899639b2c286d6bc99aa6835174523 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:06:40 +0200 Subject: [PATCH 272/359] remove use_python_2 from aftereffects --- .../settings/defaults/system_settings/applications.json | 2 -- .../system_schema/host_settings/schema_aftereffects.json | 6 ++++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index d8c9e171fc..224f9dc318 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1021,7 +1021,6 @@ "2020": { "enabled": true, "variant_label": "2020", - "use_python_2": false, "executables": { "windows": [ "" @@ -1039,7 +1038,6 @@ "2021": { "enabled": true, "variant_label": "2021", - "use_python_2": false, "executables": { "windows": [ "C:\\Program Files\\Adobe\\Adobe After Effects 2021\\Support Files\\AfterFX.exe" diff --git a/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json b/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json index afadf48173..6c36a9bb8a 100644 --- a/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json +++ b/openpype/settings/entities/schemas/system_schema/host_settings/schema_aftereffects.json @@ -29,11 +29,13 @@ "template_data": [ { "app_variant_label": "2020", - "app_variant": "2020" + "app_variant": "2020", + "variant_skip_paths": ["use_python_2"] }, { "app_variant_label": "2021", - "app_variant": "2021" + "app_variant": "2021", + "variant_skip_paths": ["use_python_2"] } ] } From cd1bf6d302e2f369b2a3cca0a21b64a85c8d9a79 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:10:23 +0200 Subject: [PATCH 273/359] added better condition for full replacements --- openpype/settings/entities/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index cf0da29978..92510e04d5 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -129,6 +129,7 @@ def _fill_schema_template_data( elif isinstance(template, STRING_TYPE): # TODO find much better way how to handle filling template data template = template.replace("{{", "__dbcb__").replace("}}", "__decb__") + full_replacement = False for replacement_string in template_key_pattern.findall(template): key = str(replacement_string[1:-1]) required_keys.add(key) @@ -141,11 +142,12 @@ def _fill_schema_template_data( # Replace the value with value from templates data # - with this is possible to set value with different type template = value + full_replacement = True else: # Only replace the key in string template = template.replace(replacement_string, value) - if isinstance(template, STRING_TYPE): + if not full_replacement: output = template.replace("__dbcb__", "{").replace("__decb__", "}") else: output = template From d91f47084860a8c40b89f37506689a457bc99e2a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:11:26 +0200 Subject: [PATCH 274/359] handle full value replacement in template --- openpype/settings/entities/lib.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 31071a2d30..d747c3e85e 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -497,6 +497,7 @@ class SchemasHub: .replace("{{", "__dbcb__") .replace("}}", "__decb__") ) + full_replacement = False for replacement_string in template_key_pattern.findall(template): key = str(replacement_string[1:-1]) required_keys.add(key) @@ -509,11 +510,19 @@ class SchemasHub: # Replace the value with value from templates data # - with this is possible to set value with different type template = value + full_replacement = True else: # Only replace the key in string template = template.replace(replacement_string, value) - output = template.replace("__dbcb__", "{").replace("__decb__", "}") + if not full_replacement: + output = ( + template + .replace("__dbcb__", "{") + .replace("__decb__", "}") + ) + else: + output = template else: output = template From 082e453d138055a86c25e8e2ad09060f28ff5d11 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:24:08 +0200 Subject: [PATCH 275/359] fix variable name usage --- openpype/tools/settings/settings/item_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index b23372e9ac..82afbb0a13 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -145,7 +145,7 @@ class DictImmutableKeysWidget(BaseWidget): self.content_widget = content_widget self.content_layout = content_layout - if len(self.input_fields) == 1 and self.checkbox_widget: + if len(self.input_fields) == 1 and self.checkbox_child: body_widget.hide_toolbox(hide_content=True) elif self.entity.collapsible: From 905db947bbf3ae5d53aade3c16f1b2de08c63def Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:40:49 +0200 Subject: [PATCH 276/359] added dict-conditional to readme --- openpype/settings/entities/schemas/README.md | 97 ++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index bbd53fa46b..3c360b892f 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -181,6 +181,103 @@ } ``` +## dict-conditional +- is similar to `dict` but has only one child entity that will be always available +- the one entity is enumerator of possible values and based on value of the entity are defined and used other children entities +- each value of enumerator have defined children that will be used + - there is no way how to have shared entities across multiple enum items +- value from enumerator is also stored next to other values + - to define the key under which will be enum value stored use `enum_key` + - `enum_key` must match key regex and any enum item can't have children with same key + - `enum_label` is label of the entity for UI purposes +- enum items are define with `enum_children` + - it's a list where each item represents enum item + - all items in `enum_children` must have at least `key` key which represents value stored under `enum_key` + - items can define `label` for UI purposes + - most important part is that item can define `children` key where are definitions of it's children (`children` value works the same way as in `dict`) +- entity must have defined `"label"` if is not used as widget +- is set as group if any parent is not group +- if `"label"` is entetered there which will be shown in GUI + - item with label can be collapsible + - that can be set with key `"collapsible"` as `True`/`False` (Default: `True`) + - with key `"collapsed"` as `True`/`False` can be set that is collapsed when GUI is opened (Default: `False`) + - it is possible to add darker background with `"highlight_content"` (Default: `False`) + - darker background has limits of maximum applies after 3-4 nested highlighted items there is not difference in the color + - output is dictionary `{the "key": children values}` +``` +# Example +{ + "type": "dict-conditional", + "key": "my_key", + "label": "My Key", + "enum_key": "type", + "enum_label": "label", + "enum_children": [ + # Each item must be a dictionary with 'key' + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "command", + "label": "Comand" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "key": "children", + "label": "Children", + "type": "list", + "object_type": "text" + } + ] + }, + { + # Separator does not have children as "separator" value is enough + "key": "separator", + "label": "Separator" + } + ] +} +``` + +How output of the schema could look like on save: +``` +{ + "type": "separator" +} + +{ + "type": "action", + "key": "action_1", + "label": "Action 1", + "command": "run command -arg" +} + +{ + "type": "menu", + "children": [ + "child_1", + "child_2" + ] +} +``` + ## Inputs for setting any kind of value (`Pure` inputs) - all these input must have defined `"key"` under which will be stored and `"label"` which will be shown next to input - unless they are used in different types of inputs (later) "as widgets" in that case `"key"` and `"label"` are not required as there is not place where to set them From df37c2e1fffbeea1baf66c5061ac2699ed9d5a7c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 14:50:07 +0200 Subject: [PATCH 277/359] removed example schema --- .../settings/entities/dict_conditional.py | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 9ba24cf0de..0e6540e606 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -24,54 +24,6 @@ from .exceptions import ( ) -example_schema = { - "type": "dict-conditional", - "key": "KEY", - "label": "LABEL", - "enum_key": "type", - "enum_label": "label", - "enum_children": [ - { - "key": "action", - "label": "Action", - "children": [ - { - "type": "text", - "key": "key", - "label": "Key" - }, - { - "type": "text", - "key": "label", - "label": "Label" - }, - { - "type": "text", - "key": "command", - "label": "Comand" - } - ] - }, - { - "key": "menu", - "label": "Menu", - "children": [ - { - "key": "children", - "label": "Children", - "type": "list", - "object_type": "text" - } - ] - }, - { - "key": "separator", - "label": "Separator" - } - ] -} - - class DictConditionalEntity(ItemEntity): """Entity represents dictionay with only one persistent key definition. From 11e8e3a8cb269d7d6de06aca6d493f7f8258fc16 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 29 Jun 2021 16:43:20 +0200 Subject: [PATCH 278/359] drop support for <4.26 --- openpype/hosts/unreal/README.md | 9 ++ openpype/hosts/unreal/api/lib.py | 122 +++++------------- .../unreal/hooks/pre_workfile_preparation.py | 23 ++-- .../system_settings/applications.json | 2 +- .../schema_project_unreal.json | 5 - 5 files changed, 55 insertions(+), 106 deletions(-) create mode 100644 openpype/hosts/unreal/README.md diff --git a/openpype/hosts/unreal/README.md b/openpype/hosts/unreal/README.md new file mode 100644 index 0000000000..0a69b9e0cf --- /dev/null +++ b/openpype/hosts/unreal/README.md @@ -0,0 +1,9 @@ +## Unreal Integration + +Supported Unreal Engine version is 4.26+ (mainly because of major Python changes done there). + +### Project naming +Unreal doesn't support project names starting with non-alphabetic character. So names like `123_myProject` are +invalid. If OpenPype detects such name it automatically prepends letter **P** to make it valid name, so `123_myProject` +will become `P123_myProject`. There is also soft-limit on project name length to be shorter than 20 characters. +Longer names will issue warning in Unreal Editor that there might be possible side effects. \ No newline at end of file diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 6231fd6f33..144f781171 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -70,6 +70,21 @@ def get_engine_versions(env=None): return OrderedDict() +def get_editor_executable_path(engine_path: Path) -> Path: + """Get UE4 Editor executable path.""" + ue4_path = engine_path / "Engine/Binaries" + if platform.system().lower() == "windows": + ue4_path /= "Win64/UE4Editor.exe" + + elif platform.system().lower() == "linux": + ue4_path /= "Linux/UE4Editor" + + elif platform.system().lower() == "darwin": + ue4_path /= "Mac/UE4Editor" + + return ue4_path + + def _win_get_engine_versions(): """Get Unreal Engine versions on Windows. @@ -244,39 +259,6 @@ def create_unreal_project(project_name: str, ] } - if preset["install_unreal_python_engine"]: - # WARNING: This is deprecated as Unreal Engine Python project - # is on hold and is mainly replaced in 4.26 by Epics own - # Python implementation. - # --------------------------------------------------------------- - # If `OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN` is set, copy it from - # there to support offline installation. - # Otherwise clone UnrealEnginePython to Plugins directory - # https://github.com/20tab/UnrealEnginePython.git - uep_path = plugins_path / "UnrealEnginePython" - if env.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"): - - os.makedirs(uep_path, exist_ok=True) - dir_util._path_created = {} - dir_util.copy_tree( - env.get("OPENPYPE_UNREAL_ENGINE_PYTHON_PLUGIN"), - uep_path.as_posix()) - else: - # WARNING: this will trigger dev_mode, because we need to compile - # this plugin. - dev_mode = True - import git - git.Repo.clone_from( - "https://github.com/20tab/UnrealEnginePython.git", - uep_path.as_posix()) - - data["Plugins"].append( - {"Name": "UnrealEnginePython", "Enabled": True}) - - if not (uep_path / "Binaries").is_dir() \ - or not (uep_path / "Intermediate").is_dir(): - dev_mode = True - if dev_mode or preset["dev_mode"]: # this will add project module and necessary source file to make it # C++ project and to (hopefully) make Unreal Editor to compile all @@ -289,73 +271,31 @@ def create_unreal_project(project_name: str, "AdditionalDependencies": ["Engine"], }] - if preset["install_unreal_python_engine"]: - # now we need to fix python path in: - # `UnrealEnginePython.Build.cs` - # to point to our python - with open(uep_path / "Source" / "UnrealEnginePython" / - "UnrealEnginePython.Build.cs", mode="r") as f: - build_file = f.read() - - fix = build_file.replace( - 'private string pythonHome = "";', - 'private string pythonHome = "{}";'.format( - sys.base_prefix.replace("\\", "/"))) - - with open(uep_path / "Source" / "UnrealEnginePython" / - "UnrealEnginePython.Build.cs", mode="w") as f: - f.write(fix) - # write project file project_file = pr_dir / f"{project_name}.uproject" with open(project_file, mode="w") as pf: json.dump(data, pf, indent=4) - # ensure we have PySide installed in engine - # this won't work probably as pyside is no longer on pypi - # DEPRECATED: support for python 2 in UE4 is dropped. + # ensure we have PySide2 installed in engine python_path = None - if int(ue_version.split(".")[0]) == 4 and \ - int(ue_version.split(".")[1]) < 25: - if platform.system().lower() == "windows": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python/Win64/python.exe") + if platform.system().lower() == "windows": + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Win64/pythonw.exe") - if platform.system().lower() == "linux": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python/Linux/bin/python") + if platform.system().lower() == "linux": + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Linux/bin/python3") - if platform.system().lower() == "darwin": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python/Mac/bin/python") + if platform.system().lower() == "darwin": + python_path = engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Mac/bin/python3") - if python_path.exists(): - subprocess.run([python_path.as_posix(), "-m", - "pip", "install", "pyside"]) - else: - raise NotImplementedError("Unsupported platform") - else: - # install PySide2 inside newer engines - if platform.system().lower() == "windows": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Win64/pythonw.exe") - - if platform.system().lower() == "linux": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Linux/bin/python3") - - if platform.system().lower() == "darwin": - python_path = engine_path / ("Engine/Binaries/ThirdParty/" - "Python3/Mac/bin/python3") - - if python_path: - if not python_path.exists(): - raise RuntimeError( - f"Unreal Python not found at {python_path}") - subprocess.run([python_path.as_posix(), "-m", - "pip", "install", "pyside2"]) - else: - raise NotImplementedError("Unsupported platform") + if not python_path: + raise NotImplementedError("Unsupported platform") + if not python_path.exists(): + raise RuntimeError(f"Unreal Python not found at {python_path}") + subprocess.run( + [python_path.as_posix(), "-m", "pip", "install", "pyside2"]) if dev_mode or preset["dev_mode"]: _prepare_cpp_project(project_file, engine_path) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 2c4dd822bc..fcdec7a96c 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -33,6 +33,18 @@ class UnrealPrelaunchHook(PreLaunchHook): workdir = self.launch_context.env["AVALON_WORKDIR"] engine_version = self.app_name.split("/")[-1].replace("-", ".") unreal_project_name = f"{asset_name}_{task_name}" + try: + if int(engine_version.split(".")[0]) < 4 and \ + int(engine_version.split(".")[1]) < 26: + raise ApplicationLaunchFailed(( + f"{self.signature} Old unsupported version of UE4 " + f"detected - {engine_version}")) + except ValueError: + # there can be string in minor version and in that case + # int cast is failing. This probably happens only with + # early access versions and is of no concert for this check + # so lets keep it quite. + ... # Unreal is sensitive about project names longer then 20 chars if len(unreal_project_name) > 20: @@ -75,15 +87,8 @@ class UnrealPrelaunchHook(PreLaunchHook): f"detected [ {engine_version} ]" )) - ue4_path = Path(detected[engine_version]) / "Engine/Binaries" - if platform.system().lower() == "windows": - ue4_path = ue4_path / "Win64/UE4Editor.exe" - - elif platform.system().lower() == "linux": - ue4_path = ue4_path / "Linux/UE4Editor" - - elif platform.system().lower() == "darwin": - ue4_path = ue4_path / "Mac/UE4Editor" + ue4_path = unreal_lib.get_editor_executable_path( + Path(detected[engine_version])) self.launch_context.launch_args.append(ue4_path.as_posix()) project_path.mkdir(parents=True, exist_ok=True) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 72cd010cf2..fae89a36ca 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1095,7 +1095,7 @@ "unreal": { "enabled": true, "label": "Unreal Editor", - "icon": "{}/app_icons/ue4.png'", + "icon": "{}/app_icons/ue4.png", "host_name": "unreal", "environment": {}, "variants": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json index b6e94d9d03..4e197e9fc8 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_unreal.json @@ -15,11 +15,6 @@ "type": "boolean", "key": "dev_mode", "label": "Dev mode" - }, - { - "type": "boolean", - "key": "install_unreal_python_engine", - "label": "Install unreal python engine" } ] } From f6700aadf3ec3b217d7775f3f7dcde2edb8f4820 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 29 Jun 2021 16:52:20 +0200 Subject: [PATCH 279/359] fix hound --- openpype/hosts/unreal/api/lib.py | 2 -- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 1 - 2 files changed, 3 deletions(-) diff --git a/openpype/hosts/unreal/api/lib.py b/openpype/hosts/unreal/api/lib.py index 144f781171..7e34c3ff15 100644 --- a/openpype/hosts/unreal/api/lib.py +++ b/openpype/hosts/unreal/api/lib.py @@ -230,8 +230,6 @@ def create_unreal_project(project_name: str, ue_id = "{" + loaded_modules.get("BuildId") + "}" plugins_path = None - uep_path = None - if os.path.isdir(env.get("AVALON_UNREAL_PLUGIN", "")): # copy plugin to correct path under project plugins_path = pr_dir / "Plugins" diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index fcdec7a96c..01b8b6bc05 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -2,7 +2,6 @@ """Hook to launch Unreal and prepare projects.""" import os from pathlib import Path -import platform from openpype.lib import ( PreLaunchHook, From 672ee1d98db2a7adf341a63e635834ede87d139d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 16:57:01 +0200 Subject: [PATCH 280/359] store ignore_missing_defaults and reuse it on callbacks --- openpype/settings/entities/base_entity.py | 1 + openpype/settings/entities/dict_conditional.py | 1 + .../entities/dict_immutable_keys_entity.py | 1 + .../entities/dict_mutable_keys_entity.py | 18 ++++++++++++++---- openpype/settings/entities/input_entities.py | 1 + openpype/settings/entities/item_entities.py | 2 ++ openpype/settings/entities/list_entity.py | 18 ++++++++++++++---- 7 files changed, 34 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index e1cd5134e7..6c2f382403 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -136,6 +136,7 @@ class BaseItemEntity(BaseEntity): # Override state defines which values are used, saved and how. # TODO convert to private attribute self._override_state = OverrideState.NOT_DEFINED + self._ignore_missing_defaults = None # These attributes may change values during existence of an object # Default value, studio override values and project override values diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 0e6540e606..33cedd7b54 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -377,6 +377,7 @@ class DictConditionalEntity(ItemEntity): # Change has/had override states self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults # Set override state on enum entity first self.enum_entity.set_override_state(state, ignore_missing_defaults) diff --git a/openpype/settings/entities/dict_immutable_keys_entity.py b/openpype/settings/entities/dict_immutable_keys_entity.py index 2802290e68..bde5304787 100644 --- a/openpype/settings/entities/dict_immutable_keys_entity.py +++ b/openpype/settings/entities/dict_immutable_keys_entity.py @@ -266,6 +266,7 @@ class DictImmutableKeysEntity(ItemEntity): # Change has/had override states self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults for child_obj in self.non_gui_children.values(): child_obj.set_override_state(state, ignore_missing_defaults) diff --git a/openpype/settings/entities/dict_mutable_keys_entity.py b/openpype/settings/entities/dict_mutable_keys_entity.py index a5734e36b8..c3df935269 100644 --- a/openpype/settings/entities/dict_mutable_keys_entity.py +++ b/openpype/settings/entities/dict_mutable_keys_entity.py @@ -154,7 +154,9 @@ class DictMutableKeysEntity(EndpointEntity): def add_key(self, key): new_child = self._add_key(key) - new_child.set_override_state(self._override_state) + new_child.set_override_state( + self._override_state, self._ignore_missing_defaults + ) self.on_change() return new_child @@ -328,6 +330,8 @@ class DictMutableKeysEntity(EndpointEntity): # TODO change metadata self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults + # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: @@ -616,7 +620,9 @@ class DictMutableKeysEntity(EndpointEntity): if not self._can_discard_changes: return - self.set_override_state(self._override_state) + self.set_override_state( + self._override_state, self._ignore_missing_defaults + ) on_change_trigger.append(self.on_change) def _add_to_studio_default(self, _on_change_trigger): @@ -651,7 +657,9 @@ class DictMutableKeysEntity(EndpointEntity): if label: children_label_by_id[child_entity.id] = label - child_entity.set_override_state(self._override_state) + child_entity.set_override_state( + self._override_state, self._ignore_missing_defaults + ) self.children_label_by_id = children_label_by_id @@ -700,7 +708,9 @@ class DictMutableKeysEntity(EndpointEntity): if label: children_label_by_id[child_entity.id] = label - child_entity.set_override_state(self._override_state) + child_entity.set_override_state( + self._override_state, self._ignore_missing_defaults + ) self.children_label_by_id = children_label_by_id diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 9b41a26bdb..2abb7a2253 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -224,6 +224,7 @@ class InputEntity(EndpointEntity): return self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: diff --git a/openpype/settings/entities/item_entities.py b/openpype/settings/entities/item_entities.py index c52eab988f..7e84f8c801 100644 --- a/openpype/settings/entities/item_entities.py +++ b/openpype/settings/entities/item_entities.py @@ -157,6 +157,7 @@ class PathEntity(ItemEntity): return self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults self.child_obj.set_override_state(state, ignore_missing_defaults) def update_default_value(self, value): @@ -351,6 +352,7 @@ class ListStrictEntity(ItemEntity): return self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults # Ignore if is dynamic item and use default in that case if not self.is_dynamic_item and not self.is_in_dynamic_item: if state > OverrideState.DEFAULTS: diff --git a/openpype/settings/entities/list_entity.py b/openpype/settings/entities/list_entity.py index 2225523792..64bbad28a7 100644 --- a/openpype/settings/entities/list_entity.py +++ b/openpype/settings/entities/list_entity.py @@ -102,7 +102,9 @@ class ListEntity(EndpointEntity): def add_new_item(self, idx=None, trigger_change=True): child_obj = self._add_new_item(idx) - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, self._ignore_missing_defaults + ) if trigger_change: self.on_child_change(child_obj) @@ -212,6 +214,7 @@ class ListEntity(EndpointEntity): return self._override_state = state + self._ignore_missing_defaults = ignore_missing_defaults while self.children: self.children.pop(0) @@ -403,7 +406,9 @@ class ListEntity(EndpointEntity): if self.had_studio_override: child_obj.update_studio_value(item) - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, self._ignore_missing_defaults + ) if self._override_state >= OverrideState.PROJECT: self._has_project_override = self.had_project_override @@ -435,7 +440,9 @@ class ListEntity(EndpointEntity): for item in value: child_obj = self._add_new_item() child_obj.update_default_value(item) - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, self._ignore_missing_defaults + ) self._ignore_child_changes = False @@ -468,7 +475,10 @@ class ListEntity(EndpointEntity): child_obj.update_default_value(item) if self._has_studio_override: child_obj.update_studio_value(item) - child_obj.set_override_state(self._override_state) + child_obj.set_override_state( + self._override_state, + self._ignore_missing_defaults + ) self._ignore_child_changes = False From 477b4ecfcc8d421fd4334c87666d61767e049371 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 29 Jun 2021 17:19:13 +0200 Subject: [PATCH 281/359] removed unused imports --- openpype/settings/entities/dict_conditional.py | 3 --- openpype/tools/settings/settings/dict_conditional.py | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 33cedd7b54..c115cac18a 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -1,8 +1,6 @@ import copy -import collections from .lib import ( - WRAPPER_TYPES, OverrideState, NOT_SET ) @@ -14,7 +12,6 @@ from openpype.settings.constants import ( from . import ( BaseItemEntity, ItemEntity, - BoolEntity, GUIEntity ) from .exceptions import ( diff --git a/openpype/tools/settings/settings/dict_conditional.py b/openpype/tools/settings/settings/dict_conditional.py index 84288f7b5b..da2f53497e 100644 --- a/openpype/tools/settings/settings/dict_conditional.py +++ b/openpype/tools/settings/settings/dict_conditional.py @@ -1,5 +1,4 @@ -import collections -from Qt import QtWidgets, QtCore, QtGui +from Qt import QtWidgets from .widgets import ( ExpandingWidget, From f2fb9885db8359e070fe882e3ca1888a0de33d3b Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 30 Jun 2021 03:42:24 +0000 Subject: [PATCH 282/359] [Automated] Bump version --- CHANGELOG.md | 17 +++++++++++------ openpype/version.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96b90cd53e..0b69a8e2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # Changelog -## [3.2.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769) +- Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766) +- Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) @@ -19,6 +22,11 @@ **🐛 Bug fixes** +- FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775) +- Project specific environments [\#1767](https://github.com/pypeclub/OpenPype/pull/1767) +- Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764) +- Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761) +- Anatomy others templates don't cause crash [\#1758](https://github.com/pypeclub/OpenPype/pull/1758) - Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745) - hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743) - Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) @@ -36,7 +44,9 @@ **Merged pull requests:** +- Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) - TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) +- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) @@ -55,10 +65,6 @@ - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) -**Merged pull requests:** - -- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) - ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2) @@ -116,7 +122,6 @@ - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) -- Use poetry to build / publish OpenPype wheel [\#1636](https://github.com/pypeclub/OpenPype/pull/1636) # Changelog diff --git a/openpype/version.py b/openpype/version.py index fcd3b2afca..0371d5f4e3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.4" +__version__ = "3.2.0-nightly.5" From b21f827790727433393397c2931d21efb099594a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Jun 2021 11:22:43 +0200 Subject: [PATCH 283/359] added few docstrings --- openpype/settings/entities/lib.py | 53 ++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index d747c3e85e..42a08232b9 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -127,7 +127,16 @@ class SchemasHub: return self._gui_types def get_schema(self, schema_name): - """Get schema definition data by it's name.""" + """Get schema definition data by it's name. + + Returns: + dict: Copy of schema loaded from json files. + + Raises: + KeyError: When schema name is stored in loaded templates or json + file was not possible to parse or when schema name was not + found. + """ if schema_name not in self._loaded_schemas: if schema_name in self._loaded_templates: raise KeyError(( @@ -148,7 +157,16 @@ class SchemasHub: return copy.deepcopy(self._loaded_schemas[schema_name]) def get_template(self, template_name): - """Get template definition data by it's name.""" + """Get template definition data by it's name. + + Returns: + list: Copy of template items loaded from json files. + + Raises: + KeyError: When template name is stored in loaded schemas or json + file was not possible to parse or when template name was not + found. + """ if template_name not in self._loaded_templates: if template_name in self._loaded_schemas: raise KeyError(( @@ -232,7 +250,16 @@ class SchemasHub: return klass(schema_data, *args, **kwargs) def _load_types(self): - """Prepare entity types for cretion of their objects.""" + """Prepare entity types for cretion of their objects. + + Currently all classes in `openpype.settings.entities` that inherited + from `BaseEntity` are stored as loaded types. GUI types are stored to + separated attribute to not mess up api access of entities. + + TODOs: + Add more dynamic way how to add custom types from anywhere and + better handling of abstract classes. Skipping them is dangerous. + """ from openpype.settings import entities @@ -400,7 +427,25 @@ class SchemasHub: required_keys=None, missing_keys=None ): - """Fill template values with data from schema data.""" + """Fill template values with data from schema data. + + Template has more abilities than schemas. It is expected that template + will be used at multiple places (but may not). Schema represents + exactly one entity and it's children but template may represent more + entities. + + Template can have "keys to fill" from their definition. Some key may be + required and some may be optional because template has their default + values defined. + + Template also have ability to "skip paths" which means to skip entities + from it's content. A template can be used across multiple places with + different requirements. + + Raises: + SchemaTemplateMissingKeys: When fill data do not contain all + required keys for template. + """ first = False if required_keys is None: first = True From ca7d91af622636b71adb4da42694b79ab8377409 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 30 Jun 2021 13:53:15 +0200 Subject: [PATCH 284/359] fix typos --- openpype/settings/entities/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index 42a08232b9..faaacd4230 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -147,7 +147,7 @@ class SchemasHub: crashed_item = self._crashed_on_load[schema_name] raise KeyError( "Unable to parse schema file \"{}\". {}".format( - crashed_item["filpath"], crashed_item["message"] + crashed_item["filepath"], crashed_item["message"] ) ) @@ -177,7 +177,7 @@ class SchemasHub: crashed_item = self._crashed_on_load[template_name] raise KeyError( "Unable to parse templace file \"{}\". {}".format( - crashed_item["filpath"], crashed_item["message"] + crashed_item["filepath"], crashed_item["message"] ) ) @@ -345,7 +345,7 @@ class SchemasHub: " One of them crashed on load \"{}\" {}" ).format( filename, - crashed_item["filpath"], + crashed_item["filepath"], crashed_item["message"] )) From 69a3d4f046bd53f1d2e84f762e63e4619ba11dc1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 1 Jul 2021 10:44:14 +0200 Subject: [PATCH 285/359] reraise exception if AVALON_APP is not set --- openpype/lib/editorial.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/editorial.py b/openpype/lib/editorial.py index 158488dd56..8e8e365bdb 100644 --- a/openpype/lib/editorial.py +++ b/openpype/lib/editorial.py @@ -7,6 +7,8 @@ try: import opentimelineio as otio from opentimelineio import opentime as _ot except ImportError: + if not os.environ.get("AVALON_APP"): + raise otio = discover_host_vendor_module("opentimelineio") _ot = discover_host_vendor_module("opentimelineio.opentime") From 29ef07aada6ff9ee20f5b04ca25e99bcbb5b68c4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jul 2021 16:28:05 +0200 Subject: [PATCH 286/359] pr suggestion https://github.com/pypeclub/OpenPype/pull/1756#discussion_r660710802 --- .../deadline/plugins/publish/submit_nuke_deadline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index 879c92490b..fed98d8a08 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -254,9 +254,9 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): if key in os.environ}, **api.Session) # self.log.debug("enviro: {}".format(pprint(environment))) - for path in os.environ: - if path.lower().startswith('openpype_'): - environment[path] = os.environ[path] + for _path in os.environ: + if _path.lower().startswith('openpype_'): + environment[_path] = os.environ[_path] clean_environment = {} for key, value in environment.items(): From ad1891e6375a8796b98e8082f7fe577db449877d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 14:55:35 +0200 Subject: [PATCH 287/359] add list version command, fix staging and use-version --- start.py | 90 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/start.py b/start.py index 8e7c195e95..7ce7d6afe7 100644 --- a/start.py +++ b/start.py @@ -135,18 +135,36 @@ if sys.__stdout__: def _print(message: str): if message.startswith("!!! "): print("{}{}".format(term.orangered2("!!! "), message[4:])) + return if message.startswith(">>> "): print("{}{}".format(term.aquamarine3(">>> "), message[4:])) + return if message.startswith("--- "): print("{}{}".format(term.darkolivegreen3("--- "), message[4:])) - if message.startswith(" "): - print("{}{}".format(term.darkseagreen3(" "), message[4:])) + return if message.startswith("*** "): print("{}{}".format(term.gold("*** "), message[4:])) + return if message.startswith(" - "): print("{}{}".format(term.wheat(" - "), message[4:])) + return if message.startswith(" . "): print("{}{}".format(term.tan(" . "), message[4:])) + return + if message.startswith(" - "): + print("{}{}".format(term.seagreen3(" - "), message[7:])) + return + if message.startswith(" ! "): + print("{}{}".format(term.goldenrod(" ! "), message[7:])) + return + if message.startswith(" * "): + print("{}{}".format(term.aquamarine1(" * "), message[7:])) + return + if message.startswith(" "): + print("{}{}".format(term.darkseagreen3(" "), message[4:])) + return + + print(message) else: def _print(message: str): print(message) @@ -175,6 +193,17 @@ silent_commands = ["run", "igniter", "standalonepublisher", "extractenvironments"] +def list_versions(openpype_versions: list, local_version=None) -> None: + """Print list of detected versions.""" + _print(" - Detected versions:") + for v in sorted(openpype_versions): + _print(f" - {v}: {v.path}") + if not openpype_versions: + _print(" ! none in repository detected") + if local_version: + _print(f" * local version {local_version}") + + def set_openpype_global_environments() -> None: """Set global OpenPype's environments.""" import acre @@ -303,6 +332,7 @@ def _process_arguments() -> tuple: # check for `--use-version=3.0.0` argument and `--use-staging` use_version = None use_staging = False + print_versions = False for arg in sys.argv: if arg == "--use-version": _print("!!! Please use option --use-version like:") @@ -313,12 +343,19 @@ def _process_arguments() -> tuple: r"--use-version=(?P\d+\.\d+\.\d+(?:\S*)?)", arg) if m and m.group('version'): use_version = m.group('version') + _print(">>> Requested version [ {} ]".format(use_version)) sys.argv.remove(arg) + if "+staging" in use_version: + use_staging = True break if "--use-staging" in sys.argv: use_staging = True sys.argv.remove("--use-staging") + if "--list-versions" in sys.argv: + print_versions = True + sys.argv.remove("--list-versions") + # handle igniter # this is helper to run igniter before anything else if "igniter" in sys.argv: @@ -334,7 +371,7 @@ def _process_arguments() -> tuple: sys.argv.pop(idx) sys.argv.insert(idx, "tray") - return use_version, use_staging + return use_version, use_staging, print_versions def _determine_mongodb() -> str: @@ -487,7 +524,7 @@ def _find_frozen_openpype(use_version: str = None, openpype_version = openpype_versions[-1] except IndexError: _print(("!!! Something is wrong and we didn't " - "found it again.")) + "found it again.")) sys.exit(1) elif return_code != 2: _print(f" . finished ({return_code})") @@ -519,13 +556,8 @@ def _find_frozen_openpype(use_version: str = None, if found: openpype_version = sorted(found)[-1] if not openpype_version: - _print(f"!!! requested version {use_version} was not found.") - if openpype_versions: - _print(" - found: ") - for v in sorted(openpype_versions): - _print(f" - {v}: {v.path}") - - _print(f" - local version {local_version}") + _print(f"!!! Requested version {use_version} was not found.") + list_versions(openpype_versions, local_version) sys.exit(1) # test if latest detected is installed (in user data dir) @@ -560,7 +592,7 @@ def _find_frozen_openpype(use_version: str = None, return openpype_version.path -def _bootstrap_from_code(use_version): +def _bootstrap_from_code(use_version, use_staging): """Bootstrap live code (or the one coming with frozen OpenPype. Args: @@ -583,7 +615,8 @@ def _bootstrap_from_code(use_version): if use_version and use_version != local_version: version_to_use = None - openpype_versions = bootstrap.find_openpype(include_zips=True) + openpype_versions = bootstrap.find_openpype( + include_zips=True, staging=use_staging) v: OpenPypeVersion found = [v for v in openpype_versions if str(v) == use_version] if found: @@ -600,13 +633,8 @@ def _bootstrap_from_code(use_version): os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501 _openpype_root = version_to_use.path.as_posix() else: - _print(f"!!! requested version {use_version} was not found.") - if openpype_versions: - _print(" - found: ") - for v in sorted(openpype_versions): - _print(f" - {v}: {v.path}") - - _print(f" - local version {local_version}") + _print(f"!!! Requested version {use_version} was not found.") + list_versions(openpype_versions, local_version) sys.exit(1) else: os.environ["OPENPYPE_VERSION"] = local_version @@ -675,7 +703,7 @@ def boot(): # Process arguments # ------------------------------------------------------------------------ - use_version, use_staging = _process_arguments() + use_version, use_staging, print_versions = _process_arguments() if os.getenv("OPENPYPE_VERSION"): use_staging = "staging" in os.getenv("OPENPYPE_VERSION") @@ -704,6 +732,24 @@ def boot(): if not os.getenv("OPENPYPE_PATH") and openpype_path: os.environ["OPENPYPE_PATH"] = openpype_path + if print_versions: + if not use_staging: + _print("--- This will list only non-staging versions detected.") + _print(" To see staging versions, use --use-staging argument.") + else: + _print("--- This will list only staging versions detected.") + _print(" To see other version, omit --use-staging argument.") + _openpype_root = OPENPYPE_ROOT + openpype_versions = bootstrap.find_openpype(include_zips=True, + staging=use_staging) + if getattr(sys, 'frozen', False): + local_version = bootstrap.get_version(Path(_openpype_root)) + else: + local_version = bootstrap.get_local_live_version() + + list_versions(openpype_versions, local_version) + sys.exit(1) + # ------------------------------------------------------------------------ # Find OpenPype versions # ------------------------------------------------------------------------ @@ -718,7 +764,7 @@ def boot(): _print(f"!!! {e}") sys.exit(1) else: - version_path = _bootstrap_from_code(use_version) + version_path = _bootstrap_from_code(use_version, use_staging) # set this to point either to `python` from venv in case of live code # or to `openpype` or `openpype_console` in case of frozen code From 8c342f5bd65dde388ed513030b7c49c76ddc6c43 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 14:56:22 +0200 Subject: [PATCH 288/359] fix version resolution --- igniter/bootstrap_repos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 6eaea27116..8c081b8614 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -657,7 +657,7 @@ class BootstrapRepos: ] # remove duplicates - openpype_versions = list(set(openpype_versions)) + openpype_versions = sorted(list(set(openpype_versions))) return openpype_versions From 920b94fa7f4a6cdb8ecdf40751a6e765478a227d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 14:56:41 +0200 Subject: [PATCH 289/359] add more info to tests --- tests/igniter/test_bootstrap_repos.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/tests/igniter/test_bootstrap_repos.py b/tests/igniter/test_bootstrap_repos.py index 743131acfa..740a71a5ce 100644 --- a/tests/igniter/test_bootstrap_repos.py +++ b/tests/igniter/test_bootstrap_repos.py @@ -330,8 +330,8 @@ def test_find_openpype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): assert result[-1].path == expected_path, ("not a latest version of " "OpenPype 3") + printer("testing finding OpenPype in OPENPYPE_PATH ...") monkeypatch.setenv("OPENPYPE_PATH", e_path.as_posix()) - result = fix_bootstrap.find_openpype(include_zips=True) # we should have results as file were created assert result is not None, "no OpenPype version found" @@ -349,6 +349,8 @@ def test_find_openpype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): monkeypatch.delenv("OPENPYPE_PATH", raising=False) + printer("testing finding OpenPype in user data dir ...") + # mock appdirs user_data_dir def mock_user_data_dir(*args, **kwargs): """Mock local app data dir.""" @@ -373,18 +375,7 @@ def test_find_openpype(fix_bootstrap, tmp_path_factory, monkeypatch, printer): assert result[-1].path == expected_path, ("not a latest version of " "OpenPype 2") - result = fix_bootstrap.find_openpype(e_path, include_zips=True) - assert result is not None, "no OpenPype version found" - expected_path = Path( - e_path / "{}{}{}".format( - test_versions_1[5].prefix, - test_versions_1[5].version, - test_versions_1[5].suffix - ) - ) - assert result[-1].path == expected_path, ("not a latest version of " - "OpenPype 1") - + printer("testing finding OpenPype zip/dir precedence ...") result = fix_bootstrap.find_openpype(dir_path, include_zips=True) assert result is not None, "no OpenPype versions found" expected_path = Path( From 8c034a9a68d2b1f6f1e5453a28c0be50523fcbe6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 15:04:17 +0200 Subject: [PATCH 290/359] add documentation to --list-versions --- website/docs/admin_openpype_commands.md | 2 ++ website/docs/admin_use.md | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index 324e0e8481..1a91e2e7fe 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -21,6 +21,8 @@ openpype_console --use-version=3.0.0-foo+bar `--use-staging` - to use staging versions of OpenPype. +`--list-versions [--use-staging]` - to list available versions. + For more information [see here](admin_use#run-openpype). ## Commands diff --git a/website/docs/admin_use.md b/website/docs/admin_use.md index 4a2b56e6f4..4ad08a0174 100644 --- a/website/docs/admin_use.md +++ b/website/docs/admin_use.md @@ -46,6 +46,16 @@ openpype_console --use-version=3.0.1 `--use-staging` - to specify you prefer staging version. In that case it will be used (if found) instead of production one. +:::tip List available versions +To list all available versions, use: + +```shell +openpype_console --list-versions +``` + +You can add `--use-staging` to list staging versions. +::: + ### Details When you run OpenPype from executable, few check are made: From 07a18a15b2207f46dabf98eb0fc5ab4f095ca081 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 16:08:10 +0200 Subject: [PATCH 291/359] fix argument --- start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.py b/start.py index 7ce7d6afe7..cd6be0a7dd 100644 --- a/start.py +++ b/start.py @@ -537,7 +537,7 @@ def _find_frozen_openpype(use_version: str = None, _print("*** Still no luck finding OpenPype.") _print(("*** We'll try to use the one coming " "with OpenPype installation.")) - version_path = _bootstrap_from_code(use_version) + version_path = _bootstrap_from_code(use_version, use_staging) openpype_version = OpenPypeVersion( version=BootstrapRepos.get_version(version_path), path=version_path) From ad3acb706c0522888bd3d57926da59026a225bd7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 16:45:47 +0200 Subject: [PATCH 292/359] always create log file during build --- tools/build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index c8c2f392ad..89795b0a50 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -185,9 +185,9 @@ Write-Host "Building OpenPype ..." $startTime = [int][double]::Parse((Get-Date -UFormat %s)) $out = & poetry run python setup.py build 2>&1 +Set-Content -Path "$($openpype_root)\build\build.log" -Value $out if ($LASTEXITCODE -ne 0) { - Set-Content -Path "$($openpype_root)\build\build.log" -Value $out Write-Host "!!! " -NoNewLine -ForegroundColor Red Write-Host "Build failed. Check the log: " -NoNewline Write-Host ".\build\build.log" -ForegroundColor Yellow From c2b6f199c61a4e66e0cdd963a80d6967038eca4f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 16:46:52 +0200 Subject: [PATCH 293/359] fail if invalid version format requested --- start.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/start.py b/start.py index cd6be0a7dd..b2fff4eac0 100644 --- a/start.py +++ b/start.py @@ -348,6 +348,12 @@ def _process_arguments() -> tuple: if "+staging" in use_version: use_staging = True break + else: + _print("!!! Requested version isn't in correct format.") + _print((" Use --list-versions to find out" + " proper version string.")) + sys.exit(1) + if "--use-staging" in sys.argv: use_staging = True sys.argv.remove("--use-staging") @@ -607,7 +613,8 @@ def _bootstrap_from_code(use_version, use_staging): _openpype_root = OPENPYPE_ROOT if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(_openpype_root)) - _print(f" - running version: {local_version}") + switch_str = f" - will switch to {use_version}" if use_version else "" + _print(f" - booting version: {local_version}{switch_str}") assert local_version else: # get current version of OpenPype @@ -706,8 +713,13 @@ def boot(): use_version, use_staging, print_versions = _process_arguments() if os.getenv("OPENPYPE_VERSION"): - use_staging = "staging" in os.getenv("OPENPYPE_VERSION") - use_version = os.getenv("OPENPYPE_VERSION") + if use_version: + _print(("*** environment variable OPENPYPE_VERSION" + "is overridden by command line argument.")) + else: + _print(">>> version set by environment variable") + use_staging = "staging" in os.getenv("OPENPYPE_VERSION") + use_version = os.getenv("OPENPYPE_VERSION") # ------------------------------------------------------------------------ # Determine mongodb connection From 3b71660224f440e280671d4d4d43ef0b79a7ecbd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 16:49:20 +0200 Subject: [PATCH 294/359] fix staging info --- start.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/start.py b/start.py index b2fff4eac0..0b8528bade 100644 --- a/start.py +++ b/start.py @@ -812,7 +812,7 @@ def boot(): from openpype.version import __version__ assert version_path, "Version path not defined." - info = get_info() + info = get_info(use_staging) info.insert(0, f">>> Using OpenPype from [ {version_path} ]") t_width = 20 @@ -839,7 +839,7 @@ def boot(): sys.exit(1) -def get_info() -> list: +def get_info(use_staging=None) -> list: """Print additional information to console.""" from openpype.lib.mongo import get_default_components from openpype.lib.log import PypeLogger @@ -847,7 +847,7 @@ def get_info() -> list: components = get_default_components() inf = [] - if not getattr(sys, 'frozen', False): + if use_staging: inf.append(("OpenPype variant", "staging")) else: inf.append(("OpenPype variant", "production")) From bbd6e1f24e6b9f0cd52823408f01b9ccbd26e433 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 2 Jul 2021 17:02:55 +0200 Subject: [PATCH 295/359] fix processing of `--use-version` --- start.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/start.py b/start.py index 0b8528bade..44f8e1b0da 100644 --- a/start.py +++ b/start.py @@ -339,20 +339,21 @@ def _process_arguments() -> tuple: _print(" --use-version=3.0.0") sys.exit(1) - m = re.search( - r"--use-version=(?P\d+\.\d+\.\d+(?:\S*)?)", arg) - if m and m.group('version'): - use_version = m.group('version') - _print(">>> Requested version [ {} ]".format(use_version)) - sys.argv.remove(arg) - if "+staging" in use_version: - use_staging = True - break - else: - _print("!!! Requested version isn't in correct format.") - _print((" Use --list-versions to find out" - " proper version string.")) - sys.exit(1) + if arg.startswith("--use-version="): + m = re.search( + r"--use-version=(?P\d+\.\d+\.\d+(?:\S*)?)", arg) + if m and m.group('version'): + use_version = m.group('version') + _print(">>> Requested version [ {} ]".format(use_version)) + sys.argv.remove(arg) + if "+staging" in use_version: + use_staging = True + break + else: + _print("!!! Requested version isn't in correct format.") + _print((" Use --list-versions to find out" + " proper version string.")) + sys.exit(1) if "--use-staging" in sys.argv: use_staging = True From 3ead357d79d029371d7e368bea633e6af0a80a36 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 3 Jul 2021 03:41:04 +0000 Subject: [PATCH 296/359] [Automated] Bump version --- CHANGELOG.md | 16 ++++++++++++---- openpype/version.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b69a8e2d5..4e658d6995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,18 @@ # Changelog -## [3.2.0-nightly.5](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776) - Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769) - Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766) - Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) +- Deadline: Nuke submission additional attributes [\#1756](https://github.com/pypeclub/OpenPype/pull/1756) +- Settings schema without prefill [\#1753](https://github.com/pypeclub/OpenPype/pull/1753) - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) @@ -22,7 +25,9 @@ **🐛 Bug fixes** +- Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) - FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775) +- Fix - single file files are str only, cast it to list to count properly [\#1772](https://github.com/pypeclub/OpenPype/pull/1772) - Project specific environments [\#1767](https://github.com/pypeclub/OpenPype/pull/1767) - Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764) - Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761) @@ -46,7 +51,7 @@ - Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) - TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) -- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) +- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) @@ -61,6 +66,10 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) +**🚀 Enhancements** + +- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) + **🐛 Bug fixes** - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) @@ -88,7 +97,6 @@ **🚀 Enhancements** -- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) - OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) - Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) @@ -116,10 +124,10 @@ - Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) - Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) - Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) -- New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644) **Merged pull requests:** +- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) diff --git a/openpype/version.py b/openpype/version.py index 0371d5f4e3..86d62a83d0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.5" +__version__ = "3.2.0-nightly.6" From ec0a57f87667d583b55d826e9b1a06b6c94f0c68 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 10:41:37 +0200 Subject: [PATCH 297/359] make sure email is not None but string --- .../event_handlers_server/action_clone_review_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py index d29316c795..e165466d00 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py +++ b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py @@ -16,11 +16,12 @@ def clone_review_session(session, entity): # Add all invitees. for invitee in entity["review_session_invitees"]: + email = invitee["email"] or "" session.create( "ReviewSessionInvitee", { "name": invitee["name"], - "email": invitee["email"], + "email": email, "review_session": review_session } ) From 3d086c967a0dd79c5c320bf4a552e2de8dab9425 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 10:46:11 +0200 Subject: [PATCH 298/359] added comment --- .../ftrack/event_handlers_server/action_clone_review_session.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py index e165466d00..59c8bffb75 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py +++ b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py @@ -16,6 +16,7 @@ def clone_review_session(session, entity): # Add all invitees. for invitee in entity["review_session_invitees"]: + # Make sure email is not None but string email = invitee["email"] or "" session.create( "ReviewSessionInvitee", From 11da3d0d4dc26992e8dd2fcd4344d46aa19a57f6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Jul 2021 12:12:11 +0200 Subject: [PATCH 299/359] add `--list-versions` help --- openpype/cli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/cli.py b/openpype/cli.py index 48951c7287..ec5b04c468 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -15,6 +15,9 @@ from .pype_commands import PypeCommands expose_value=False, help="use specified version") @click.option("--use-staging", is_flag=True, expose_value=False, help="use staging variants") +@click.option("--list-versions", is_flag=True, expose_value=False, + help=("list all detected versions. Use With `--use-staging " + "to list staging versions.")) def main(ctx): """Pype is main command serving as entry point to pipeline system. From 9d8b3d222442200b15f770c01ee80ef43c80f468 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Jul 2021 12:46:32 +0200 Subject: [PATCH 300/359] fix standalone `--use-staging` --- start.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/start.py b/start.py index 44f8e1b0da..1b5c25ae3a 100644 --- a/start.py +++ b/start.py @@ -621,10 +621,26 @@ def _bootstrap_from_code(use_version, use_staging): # get current version of OpenPype local_version = bootstrap.get_local_live_version() - if use_version and use_version != local_version: - version_to_use = None - openpype_versions = bootstrap.find_openpype( - include_zips=True, staging=use_staging) + version_to_use = None + openpype_versions = bootstrap.find_openpype( + include_zips=True, staging=use_staging) + if use_staging and not use_version: + try: + version_to_use = openpype_versions[-1] + except IndexError: + _print("!!! No staging versions are found.") + list_versions(openpype_versions, local_version) + sys.exit(1) + if version_to_use.path.is_file(): + version_to_use.path = bootstrap.extract_openpype( + version_to_use) + bootstrap.add_paths_from_directory(version_to_use.path) + os.environ["OPENPYPE_VERSION"] = str(version_to_use) + version_path = version_to_use.path + os.environ["OPENPYPE_REPOS_ROOT"] = (version_path / "openpype").as_posix() # noqa: E501 + _openpype_root = version_to_use.path.as_posix() + + elif use_version and use_version != local_version: v: OpenPypeVersion found = [v for v in openpype_versions if str(v) == use_version] if found: From f0e6bb7a82a98e040f47087bdb00fc7ffed47cbc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:42:30 +0200 Subject: [PATCH 301/359] created copy of multiple notes action --- .../action_multiple_notes.py | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py new file mode 100644 index 0000000000..8db65fe39b --- /dev/null +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -0,0 +1,110 @@ +from openpype.modules.ftrack.lib import BaseAction, statics_icon + + +class MultipleNotes(BaseAction): + '''Edit meta data action.''' + + #: Action identifier. + identifier = 'multiple.notes' + #: Action label. + label = 'Multiple Notes' + #: Action description. + description = 'Add same note to multiple Asset Versions' + icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg") + + def discover(self, session, entities, event): + ''' Validation ''' + valid = True + for entity in entities: + if entity.entity_type.lower() != 'assetversion': + valid = False + break + return valid + + def interface(self, session, entities, event): + if not event['data'].get('values', {}): + note_label = { + 'type': 'label', + 'value': '# Enter note: #' + } + + note_value = { + 'name': 'note', + 'type': 'textarea' + } + + category_label = { + 'type': 'label', + 'value': '## Category: ##' + } + + category_data = [] + category_data.append({ + 'label': '- None -', + 'value': 'none' + }) + all_categories = session.query('NoteCategory').all() + for cat in all_categories: + category_data.append({ + 'label': cat['name'], + 'value': cat['id'] + }) + category_value = { + 'type': 'enumerator', + 'name': 'category', + 'data': category_data, + 'value': 'none' + } + + splitter = { + 'type': 'label', + 'value': '{}'.format(200*"-") + } + + items = [] + items.append(note_label) + items.append(note_value) + items.append(splitter) + items.append(category_label) + items.append(category_value) + return items + + def launch(self, session, entities, event): + if 'values' not in event['data']: + return + + values = event['data']['values'] + if len(values) <= 0 or 'note' not in values: + return False + # Get Note text + note_value = values['note'] + if note_value.lower().strip() == '': + return False + # Get User + user = session.query( + 'User where username is "{}"'.format(session.api_user) + ).one() + # Base note data + note_data = { + 'content': note_value, + 'author': user + } + # Get category + category_value = values['category'] + if category_value != 'none': + category = session.query( + 'NoteCategory where id is "{}"'.format(category_value) + ).one() + note_data['category'] = category + # Create notes for entities + for entity in entities: + new_note = session.create('Note', note_data) + entity['notes'].append(new_note) + session.commit() + return True + + +def register(session): + '''Register plugin. Called when used as an plugin.''' + + MultipleNotes(session).register() From 4a2efb1cd2ae9b617ceaf8ddd45eb1b304e7de2f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:53:06 +0200 Subject: [PATCH 302/359] Converted to server action --- .../action_multiple_notes.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 8db65fe39b..021a61e0ce 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -1,16 +1,12 @@ -from openpype.modules.ftrack.lib import BaseAction, statics_icon +from openpype.modules.ftrack.lib import ServerAction -class MultipleNotes(BaseAction): - '''Edit meta data action.''' +class MultipleNotesServer(ServerAction): + """Action adds same note for muliple AssetVersions.""" - #: Action identifier. - identifier = 'multiple.notes' - #: Action label. - label = 'Multiple Notes' - #: Action description. - description = 'Add same note to multiple Asset Versions' - icon = statics_icon("ftrack", "action_icons", "MultipleNotes.svg") + identifier = "multiple.notes.server" + label = "Multiple Notes (Server)" + description = "Add same note to multiple Asset Versions" def discover(self, session, entities, event): ''' Validation ''' @@ -107,4 +103,4 @@ class MultipleNotes(BaseAction): def register(session): '''Register plugin. Called when used as an plugin.''' - MultipleNotes(session).register() + MultipleNotesServer(session).register() From c1994950adb7cc28704f409c3486b5a875d36311 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:54:07 +0200 Subject: [PATCH 303/359] use user from event instead of session event --- .../action_multiple_notes.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 021a61e0ce..c41b900031 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -18,6 +18,15 @@ class MultipleNotesServer(ServerAction): return valid def interface(self, session, entities, event): + event_source = event["source"] + user_info = event_source.get("user") or {} + user_id = user_info.get("id") + if not user_id: + return { + "success": False, + "message": "Couldn't get user information." + } + if not event['data'].get('values', {}): note_label = { 'type': 'label', @@ -77,9 +86,21 @@ class MultipleNotesServer(ServerAction): if note_value.lower().strip() == '': return False # Get User - user = session.query( - 'User where username is "{}"'.format(session.api_user) - ).one() + event_source = event["source"] + user_info = event_source.get("user") or {} + user_id = user_info.get("id") + user = None + if user_id: + user = session.query( + 'User where id is "{}"'.format(user_id) + ).first() + + if not user: + return { + "success": False, + "message": "Couldn't get user information." + } + # Base note data note_data = { 'content': note_value, From 2710bce4bd1d69ef20c8867db8db13f3dfb2f0be Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:57:46 +0200 Subject: [PATCH 304/359] use constant for none category --- .../ftrack/event_handlers_server/action_multiple_notes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index c41b900031..64b5161366 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -8,6 +8,8 @@ class MultipleNotesServer(ServerAction): label = "Multiple Notes (Server)" description = "Add same note to multiple Asset Versions" + _none_category = "__NONE__" + def discover(self, session, entities, event): ''' Validation ''' valid = True @@ -46,7 +48,7 @@ class MultipleNotesServer(ServerAction): category_data = [] category_data.append({ 'label': '- None -', - 'value': 'none' + "value": self._none_category }) all_categories = session.query('NoteCategory').all() for cat in all_categories: @@ -58,7 +60,7 @@ class MultipleNotesServer(ServerAction): 'type': 'enumerator', 'name': 'category', 'data': category_data, - 'value': 'none' + "value": self._none_category } splitter = { @@ -108,7 +110,7 @@ class MultipleNotesServer(ServerAction): } # Get category category_value = values['category'] - if category_value != 'none': + if category_value != self._none_category: category = session.query( 'NoteCategory where id is "{}"'.format(category_value) ).one() From 61588d9ddd317213dd0795c69b8fd8f22ee898ea Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:58:56 +0200 Subject: [PATCH 305/359] formatting changes --- .../action_multiple_notes.py | 70 ++++++++++--------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 64b5161366..d08e66ff56 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -14,7 +14,7 @@ class MultipleNotesServer(ServerAction): ''' Validation ''' valid = True for entity in entities: - if entity.entity_type.lower() != 'assetversion': + if entity.entity_type.lower() != "assetversion": valid = False break return valid @@ -31,62 +31,64 @@ class MultipleNotesServer(ServerAction): if not event['data'].get('values', {}): note_label = { - 'type': 'label', - 'value': '# Enter note: #' + "type": "label", + "value": "# Enter note: #" } note_value = { - 'name': 'note', - 'type': 'textarea' + "name": "note", + "type": "textarea" } category_label = { - 'type': 'label', - 'value': '## Category: ##' + "type": "label", + "value": "## Category: ##" } category_data = [] category_data.append({ - 'label': '- None -', + "label": "- None -", "value": self._none_category }) all_categories = session.query('NoteCategory').all() for cat in all_categories: category_data.append({ - 'label': cat['name'], - 'value': cat['id'] + "label": cat["name"], + "value": cat["id"] }) category_value = { - 'type': 'enumerator', - 'name': 'category', - 'data': category_data, + "type": "enumerator", + "name": "category", + "data": category_data, "value": self._none_category } splitter = { - 'type': 'label', - 'value': '{}'.format(200*"-") + "type": "label", + "value": "---" } - items = [] - items.append(note_label) - items.append(note_value) - items.append(splitter) - items.append(category_label) - items.append(category_value) - return items + return [ + note_label, + note_value, + splitter, + category_label, + category_value + ] def launch(self, session, entities, event): - if 'values' not in event['data']: + if "values" not in event["data"]: return - values = event['data']['values'] - if len(values) <= 0 or 'note' not in values: + values = event["data"]["values"] + if len(values) <= 0 or "note" not in values: return False + # Get Note text - note_value = values['note'] - if note_value.lower().strip() == '': + note_value = values["note"] + if note_value.lower().strip() == "": return False + # Get User event_source = event["source"] user_info = event_source.get("user") or {} @@ -105,20 +107,20 @@ class MultipleNotesServer(ServerAction): # Base note data note_data = { - 'content': note_value, - 'author': user + "content": note_value, + "author": user } # Get category - category_value = values['category'] + category_value = values["category"] if category_value != self._none_category: category = session.query( - 'NoteCategory where id is "{}"'.format(category_value) + "NoteCategory where id is \"{}\"".format(category_value) ).one() - note_data['category'] = category + note_data["category"] = category # Create notes for entities for entity in entities: - new_note = session.create('Note', note_data) - entity['notes'].append(new_note) + new_note = session.create("Note", note_data) + entity["notes"].append(new_note) session.commit() return True From 52556fc9cea02ce331ba59ef556ccf6bb2f7dbaf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 17:59:28 +0200 Subject: [PATCH 306/359] reversed logic of interface --- .../action_multiple_notes.py | 83 ++++++++++--------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index d08e66ff56..7b8e883174 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -29,52 +29,57 @@ class MultipleNotesServer(ServerAction): "message": "Couldn't get user information." } - if not event['data'].get('values', {}): - note_label = { - "type": "label", - "value": "# Enter note: #" - } + values = event["data"].get("values") + if values: + return None - note_value = { - "name": "note", - "type": "textarea" - } + note_label = { + "type": "label", + "value": "# Enter note: #" + } - category_label = { - "type": "label", - "value": "## Category: ##" - } + note_value = { + "name": "note", + "type": "textarea" + } - category_data = [] + category_label = { + "type": "label", + "value": "## Category: ##" + } + + category_data = [] + category_data.append({ + "label": "- None -", + "value": self._none_category + }) + all_categories = session.query( + "select id, name from NoteCategory" + ).all() + for cat in all_categories: category_data.append({ - "label": "- None -", - "value": self._none_category + "label": cat["name"], + "value": cat["id"] }) - all_categories = session.query('NoteCategory').all() - for cat in all_categories: - category_data.append({ - "label": cat["name"], - "value": cat["id"] - }) - category_value = { - "type": "enumerator", - "name": "category", - "data": category_data, - "value": self._none_category - } + category_value = { + "type": "enumerator", + "name": "category", + "data": category_data, + "value": self._none_category + } - splitter = { - "type": "label", - "value": "---" - } + splitter = { + "type": "label", + "value": "---" + } - return [ - note_label, - note_value, - splitter, - category_label, - category_value - ] + return [ + note_label, + note_value, + splitter, + category_label, + category_value + ] def launch(self, session, entities, event): if "values" not in event["data"]: From 17a97044b9bef033aefc392f7a3aa8011de0be6f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 18:23:21 +0200 Subject: [PATCH 307/359] addedd few modifications to discovery --- .../event_handlers_server/action_multiple_notes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 7b8e883174..7ed0129951 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -11,13 +11,14 @@ class MultipleNotesServer(ServerAction): _none_category = "__NONE__" def discover(self, session, entities, event): - ''' Validation ''' - valid = True + """Show action only on AssetVersions.""" + if not entities: + return False + for entity in entities: if entity.entity_type.lower() != "assetversion": - valid = False - break - return valid + return False + return True def interface(self, session, entities, event): event_source = event["source"] From baf02590c24c27961ce60657f4901d894c538a06 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 18:23:50 +0200 Subject: [PATCH 308/359] fixed few return values --- .../event_handlers_server/action_multiple_notes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 7ed0129951..1c3c9929d9 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -25,10 +25,7 @@ class MultipleNotesServer(ServerAction): user_info = event_source.get("user") or {} user_id = user_info.get("id") if not user_id: - return { - "success": False, - "message": "Couldn't get user information." - } + return None values = event["data"].get("values") if values: @@ -84,7 +81,7 @@ class MultipleNotesServer(ServerAction): def launch(self, session, entities, event): if "values" not in event["data"]: - return + return None values = event["data"]["values"] if len(values) <= 0 or "note" not in values: @@ -93,7 +90,10 @@ class MultipleNotesServer(ServerAction): # Get Note text note_value = values["note"] if note_value.lower().strip() == "": - return False + return { + "success": True, + "message": "Note was not entered. Skipping" + } # Get User event_source = event["source"] From cd3cd88dfacef90a8b0258291ad907d5f7f7ed90 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 18:25:07 +0200 Subject: [PATCH 309/359] added debug log --- .../action_multiple_notes.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 1c3c9929d9..340dd659af 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -111,18 +111,44 @@ class MultipleNotesServer(ServerAction): "message": "Couldn't get user information." } + # Logging message preparation + # - username + username = user.get("username") or "N/A" + + # - AssetVersion ids + asset_version_ids_str = ",".join([entity["id"] for entity in entities]) + # Base note data note_data = { "content": note_value, "author": user } + # Get category - category_value = values["category"] - if category_value != self._none_category: + category_id = values["category"] + if category_id == self._none_category: + category_id = None + + category_name = None + if category_id is not None: category = session.query( - "NoteCategory where id is \"{}\"".format(category_value) - ).one() - note_data["category"] = category + "select id, name from NoteCategory where id is \"{}\"".format( + category_id + ) + ).first() + if category: + note_data["category"] = category + category_name = category["name"] + + category_msg = "" + if category_name: + category_msg = " with category: \"{}\"".format(category_name) + + self.log.warning(( + "Creating note{} as User \"{}\" on " + "AssetVersions: {} with value \"{}\"" + ).format(category_msg, username, asset_version_ids_str, note_value)) + # Create notes for entities for entity in entities: new_note = session.create("Note", note_data) From 2be9dd55ee5a52bc0efb7ec46a87d721bd0d9ee0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 18:25:14 +0200 Subject: [PATCH 310/359] enhanced docstring --- .../ftrack/event_handlers_server/action_multiple_notes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py index 340dd659af..9ad7b1a969 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py +++ b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py @@ -2,7 +2,11 @@ from openpype.modules.ftrack.lib import ServerAction class MultipleNotesServer(ServerAction): - """Action adds same note for muliple AssetVersions.""" + """Action adds same note for muliple AssetVersions. + + Note is added to selection of AssetVersions. Note is created with user + who triggered the action. It is possible to define note category of note. + """ identifier = "multiple.notes.server" label = "Multiple Notes (Server)" From afdb5a8f59d252d0f9f6938896820472ad15b891 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 7 Jul 2021 19:17:34 +0200 Subject: [PATCH 311/359] add pluginInfo and jobInfo custom settings --- .../plugins/publish/submit_maya_deadline.py | 19 +++++++++++++++++++ .../defaults/project_settings/maya.json | 4 ++++ .../schemas/schema_maya_publish.json | 18 ++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index a5841f406c..0e09641200 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -271,6 +271,21 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): ["DEADLINE_REST_URL"] ) + self._job_info = ( + context.data["project_settings"] + ["maya"] + ["publish"] + ["deadline"] + ["jobInfo"] + ) + self._plugin_info = ( + context.data["project_settings"] + ["maya"] + ["publish"] + ["deadline"] + ["pluginInfo"] + ) + assert self._deadline_url, "Requires DEADLINE_REST_URL" context = instance.context @@ -536,6 +551,10 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.preflight_check(instance) + # add jobInfo and pluginInfo variables from Settings + payload["JobInfo"].update(self._job_info) + payload["PluginInfo"].update(self._plugin_info) + # Prepare tiles data ------------------------------------------------ if instance.data.get("tileRendering"): # if we have sequence of files, we need to create tile job for diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 284a1a0040..62d8a74670 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -391,6 +391,10 @@ } } }, + "deadline": { + "jobInfo": {}, + "pluginInfo": {} + }, "ExtractCameraAlembic": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 5ca7059ee5..7e50682f5d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -309,6 +309,24 @@ "type": "schema", "name": "schema_maya_capture" }, + { + "type": "dict", + "collapsible": true, + "key": "deadline", + "label": "Additional Deadline Settings", + "children": [ + { + "type": "raw-json", + "key": "jobInfo", + "label": "Additional JobInfo data" + }, + { + "type": "raw-json", + "key": "pluginInfo", + "label": "Additional PluginInfo data" + } + ] + }, { "type": "dict", "collapsible": true, From 898157c95be9205d77934a03d5c02a7b658e3978 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:28:12 +0200 Subject: [PATCH 312/359] fixed typo --- openpype/settings/entities/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index faaacd4230..e58281644a 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -176,7 +176,7 @@ class SchemasHub: elif template_name in self._crashed_on_load: crashed_item = self._crashed_on_load[template_name] raise KeyError( - "Unable to parse templace file \"{}\". {}".format( + "Unable to parse template file \"{}\". {}".format( crashed_item["filepath"], crashed_item["message"] ) ) From ec01e148e56cc17b3b50dfe811e5cf27d4be07ef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:36:32 +0200 Subject: [PATCH 313/359] added missing attributes --- openpype/settings/entities/dict_conditional.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index c115cac18a..641986491c 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -144,6 +144,11 @@ class DictConditionalEntity(ItemEntity): self.enum_entity = None + self.highlight_content = self.schema_data.get( + "highlight_content", False + ) + self.show_borders = self.schema_data.get("show_borders", True) + self._add_children() @property From 1cb8d0f5e8b64f9d6990deebb103ddbf920eb987 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 19:37:43 +0200 Subject: [PATCH 314/359] added example of conditional dictionary --- .../schemas/system_schema/example_schema.json | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/example_schema.json b/openpype/settings/entities/schemas/system_schema/example_schema.json index a4ed56df32..c3287d7452 100644 --- a/openpype/settings/entities/schemas/system_schema/example_schema.json +++ b/openpype/settings/entities/schemas/system_schema/example_schema.json @@ -9,6 +9,54 @@ "label": "Color input", "type": "color" }, + { + "type": "dict-conditional", + "use_label_wrap": true, + "collapsible": true, + "key": "menu_items", + "label": "Menu items", + "enum_key": "type", + "enum_label": "Type", + "enum_children": [ + { + "key": "action", + "label": "Action", + "children": [ + { + "type": "text", + "key": "key", + "label": "Key" + }, + { + "type": "text", + "key": "label", + "label": "Label" + }, + { + "type": "text", + "key": "command", + "label": "Comand" + } + ] + }, + { + "key": "menu", + "label": "Menu", + "children": [ + { + "key": "children", + "label": "Children", + "type": "list", + "object_type": "text" + } + ] + }, + { + "key": "separator", + "label": "Separator" + } + ] + }, { "type": "dict", "key": "schema_template_exaples", From e8bdd1616c5aade342b02d0029d88162c9a294f6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:34:13 +0200 Subject: [PATCH 315/359] create QSettings in standalone publisher --- openpype/tools/standalonepublish/app.py | 6 +++++- openpype/tools/standalonepublish/widgets/widget_asset.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/tools/standalonepublish/app.py b/openpype/tools/standalonepublish/app.py index 169abe530a..7c3e902f6c 100644 --- a/openpype/tools/standalonepublish/app.py +++ b/openpype/tools/standalonepublish/app.py @@ -34,6 +34,8 @@ class Window(QtWidgets.QDialog): self._db = AvalonMongoDB() self._db.install() + self._settings = QtCore.QSettings("pypeclub", "StandalonePublisher") + self.pyblish_paths = pyblish_paths self.setWindowTitle("Standalone Publish") @@ -44,7 +46,9 @@ class Window(QtWidgets.QDialog): self.valid_parent = False # assets widget - widget_assets = AssetWidget(dbcon=self._db, parent=self) + widget_assets = AssetWidget( + self._settings, dbcon=self._db, parent=self + ) # family widget widget_family = FamilyWidget(dbcon=self._db, parent=self) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 4680e88344..24b93c8343 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -127,11 +127,12 @@ class AssetWidget(QtWidgets.QWidget): current_changed = QtCore.Signal() # on view current index change task_changed = QtCore.Signal() - def __init__(self, dbcon, parent=None): + def __init__(self, settings, dbcon, parent=None): super(AssetWidget, self).__init__(parent=parent) self.setContentsMargins(0, 0, 0, 0) self.dbcon = dbcon + self._settings = settings layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) From 45a6fa1ea5ab83fc6e82a6d60fea00f76990dc63 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:35:28 +0200 Subject: [PATCH 316/359] store projects on project change to settings --- .../tools/standalonepublish/widgets/widget_asset.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 24b93c8343..683b05d836 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -255,6 +255,16 @@ class AssetWidget(QtWidgets.QWidget): project_name = self.combo_projects.currentText() if project_name in projects: self.dbcon.Session["AVALON_PROJECT"] = project_name + last_projects = [ + value + for value in self._settings.value("projects", "").split("|") + ] + if project_name in last_projects: + last_projects.remove(project_name) + last_projects.insert(0, project_name) + while len(last_projects) > 5: + last_projects.pop(-1) + self._settings.setValue("projects", "|".join(last_projects)) self.project_changed.emit(project_name) From 07abf855ddb40cac1ade2bc824f1784dd6f6d840 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:35:42 +0200 Subject: [PATCH 317/359] load last projects on initialization of projects combobox --- .../standalonepublish/widgets/widget_asset.py | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 683b05d836..0070488b3e 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -239,14 +239,31 @@ class AssetWidget(QtWidgets.QWidget): return output def _set_projects(self): - projects = list() + project_names = list() for project in self.dbcon.projects(): - projects.append(project['name']) + project_name = project.get("name") + if project_name: + project_names.append(project_name) self.combo_projects.clear() - if len(projects) > 0: - self.combo_projects.addItems(projects) - self.dbcon.Session["AVALON_PROJECT"] = projects[0] + + if not project_names: + return + + sorted_project_names = list(sorted(project_names)) + self.combo_projects.addItems(list(sorted(sorted_project_names))) + + last_projects = self._settings.value("projects", "") + last_project = sorted_project_names[0] + for project_name in last_projects.split("|"): + if project_name in sorted_project_names: + last_project = project_name + break + + index = sorted_project_names.index(last_project) + self.combo_projects.setCurrentIndex(index) + + self.dbcon.Session["AVALON_PROJECT"] = last_project def on_project_change(self): projects = list() From 72ec36239b2ad223e9e2ea64c58d17f51988715e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:52:00 +0200 Subject: [PATCH 318/359] load and store last projects is more secure --- openpype/tools/standalonepublish/app.py | 11 +++-- .../standalonepublish/widgets/widget_asset.py | 44 +++++++++++++------ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/openpype/tools/standalonepublish/app.py b/openpype/tools/standalonepublish/app.py index 7c3e902f6c..81a53c52b8 100644 --- a/openpype/tools/standalonepublish/app.py +++ b/openpype/tools/standalonepublish/app.py @@ -34,7 +34,12 @@ class Window(QtWidgets.QDialog): self._db = AvalonMongoDB() self._db.install() - self._settings = QtCore.QSettings("pypeclub", "StandalonePublisher") + try: + settings = QtCore.QSettings("pypeclub", "StandalonePublisher") + except Exception: + settings = None + + self._settings = settings self.pyblish_paths = pyblish_paths @@ -46,9 +51,7 @@ class Window(QtWidgets.QDialog): self.valid_parent = False # assets widget - widget_assets = AssetWidget( - self._settings, dbcon=self._db, parent=self - ) + widget_assets = AssetWidget(self._db, settings, self) # family widget widget_family = FamilyWidget(dbcon=self._db, parent=self) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 0070488b3e..8fb0d452bd 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -127,7 +127,7 @@ class AssetWidget(QtWidgets.QWidget): current_changed = QtCore.Signal() # on view current index change task_changed = QtCore.Signal() - def __init__(self, settings, dbcon, parent=None): + def __init__(self, dbcon, settings, parent=None): super(AssetWidget, self).__init__(parent=parent) self.setContentsMargins(0, 0, 0, 0) @@ -238,6 +238,34 @@ class AssetWidget(QtWidgets.QWidget): output.extend(self.get_parents(parent)) return output + def _get_last_projects(self): + if not self._settings: + return [] + + project_names = [] + for project_name in self._settings.value("projects", "").split("|"): + if project_name: + project_names.append(project_name) + return project_names + + def _add_last_project(self, project_name): + if not self._settings: + return + + last_projects = [] + for _project_name in self._settings.value("projects", "").split("|"): + if _project_name: + last_projects.append(_project_name) + + if project_name in last_projects: + last_projects.remove(project_name) + + last_projects.insert(0, project_name) + while len(last_projects) > 5: + last_projects.pop(-1) + + self._settings.setValue("projects", "|".join(last_projects)) + def _set_projects(self): project_names = list() for project in self.dbcon.projects(): @@ -253,9 +281,8 @@ class AssetWidget(QtWidgets.QWidget): sorted_project_names = list(sorted(project_names)) self.combo_projects.addItems(list(sorted(sorted_project_names))) - last_projects = self._settings.value("projects", "") last_project = sorted_project_names[0] - for project_name in last_projects.split("|"): + for project_name in self._get_last_projects(): if project_name in sorted_project_names: last_project = project_name break @@ -272,16 +299,7 @@ class AssetWidget(QtWidgets.QWidget): project_name = self.combo_projects.currentText() if project_name in projects: self.dbcon.Session["AVALON_PROJECT"] = project_name - last_projects = [ - value - for value in self._settings.value("projects", "").split("|") - ] - if project_name in last_projects: - last_projects.remove(project_name) - last_projects.insert(0, project_name) - while len(last_projects) > 5: - last_projects.pop(-1) - self._settings.setValue("projects", "|".join(last_projects)) + self._add_last_project(project_name) self.project_changed.emit(project_name) From f973833153676dc4dc39e43d530eb1d96e8af4f5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 7 Jul 2021 20:54:21 +0200 Subject: [PATCH 319/359] added delegate to project combobox --- openpype/tools/standalonepublish/widgets/widget_asset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 8fb0d452bd..c39d71b055 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -140,6 +140,10 @@ class AssetWidget(QtWidgets.QWidget): # Project self.combo_projects = QtWidgets.QComboBox() + # Change delegate so stylysheets are applied + project_delegate = QtWidgets.QStyledItemDelegate(self.combo_projects) + self.combo_projects.setItemDelegate(project_delegate) + self._set_projects() self.combo_projects.currentTextChanged.connect(self.on_project_change) # Tree View @@ -199,6 +203,7 @@ class AssetWidget(QtWidgets.QWidget): self.selection_changed.connect(self._refresh_tasks) + self.project_delegate = project_delegate self.task_view = task_view self.task_model = task_model self.refreshButton = refresh From 93b9624181a1257f7de917a16e04736130eef9e9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 11:21:05 +0200 Subject: [PATCH 320/359] fix key loaded from settings --- .../modules/ftrack/plugins/publish/collect_ftrack_family.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py index b505a429b5..8464a43ef7 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py @@ -51,7 +51,7 @@ class CollectFtrackFamily(pyblish.api.InstancePlugin): families = instance.data.get("families") add_ftrack_family = profile["add_ftrack_family"] - additional_filters = profile.get("additional_filters") + additional_filters = profile.get("advanced_filtering") if additional_filters: add_ftrack_family = self._get_add_ftrack_f_from_addit_filters( additional_filters, From 83afef7392e09010019aa5a4029046b61189cdc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 11:21:31 +0200 Subject: [PATCH 321/359] add ftrack family for review family --- openpype/settings/defaults/project_settings/ftrack.json | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 88f4e1e2e7..7f15742772 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -229,7 +229,6 @@ "standalonepublisher" ], "families": [ - "review", "plate" ], "tasks": [], From 622c6e6ca3a28e4da9251810581fd8155c9a23da Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 8 Jul 2021 12:15:09 +0200 Subject: [PATCH 322/359] nuke: fixing wrong name of family folder when `used existing frames` --- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 00d96c6cd1..662f2c808e 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -81,18 +81,18 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): if target == "Use existing frames": # Local rendering self.log.info("flagged for no render") - families.append(family) + families.append(families_ak.lower()) elif target == "Local": # Local rendering self.log.info("flagged for local render") families.append("{}.local".format(family)) + family = families_ak.lower() elif target == "On farm": # Farm rendering self.log.info("flagged for farm render") instance.data["transfer"] = False families.append("{}.farm".format(family)) - - family = families_ak.lower() + family = families_ak.lower() node.begin() for i in nuke.allNodes(): From d00eec81d4de2e47b2ee6842ec92edc3bf9c2d78 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 12:16:45 +0200 Subject: [PATCH 323/359] formatting keys fix --- openpype/hosts/houdini/plugins/publish/collect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 413553c864..2e294face2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -56,7 +56,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # Create nice name if the instance has a frame range. label = data.get("name", node.name()) if "frameStart" in data and "frameEnd" in data: - frames = "[{startFrame} - {endFrame}]".format(**data) + frames = "[{frameStart} - {frameEnd}]".format(**data) label = "{} {}".format(label, frames) instance = context.create_instance(label) From 130e9ffa502605011f2ce75d351fa2f5280f7dfe Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 8 Jul 2021 12:33:09 +0200 Subject: [PATCH 324/359] nuke: fix review family switch --- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index 00d96c6cd1..9d671646c5 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -70,8 +70,9 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin): review = False if "review" in node.knobs(): review = node["review"].value() + + if review: families.append("review") - families.append("ftrack") # Add all nodes in group instances. if node.Class() == "Group": From 97fe86cdd1faee28f67f66e418eea8ce7c7cfd6b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 8 Jul 2021 12:37:11 +0200 Subject: [PATCH 325/359] settings: Ftrack family nuke preset --- .../defaults/project_settings/ftrack.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 88f4e1e2e7..78b34d5373 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -279,6 +279,25 @@ "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] + }, + { + "hosts": [ + "nuke" + ], + "families": [ + "write", + "render" + ], + "tasks": [], + "add_ftrack_family": false, + "advanced_filtering": [ + { + "families": [ + "review" + ], + "add_ftrack_family": true + } + ] } ] }, From 54eb42f16ac024723270a3468fe72d03274b5b19 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 14:43:00 +0200 Subject: [PATCH 326/359] fix ignoring of missing defaults --- openpype/settings/entities/dict_conditional.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 641986491c..1ffc7ab450 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -384,16 +384,9 @@ class DictConditionalEntity(ItemEntity): # Set override state on enum entity first self.enum_entity.set_override_state(state, ignore_missing_defaults) - # Set override state on other entities under current enum value - for child_obj in self.non_gui_children[self.current_enum].values(): - child_obj.set_override_state(state, ignore_missing_defaults) - # Set override state on other enum children # - these must not raise exception about missing defaults for item_key, children_by_key in self.non_gui_children.items(): - if item_key == self.current_enum: - continue - for child_obj in children_by_key.values(): child_obj.set_override_state(state, True) From 696c72c34cc555a7af365c4f194a7f510b1151a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 8 Jul 2021 14:44:34 +0200 Subject: [PATCH 327/359] remove unusued variable --- openpype/settings/entities/dict_conditional.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index 1ffc7ab450..96065b670e 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -386,7 +386,7 @@ class DictConditionalEntity(ItemEntity): # Set override state on other enum children # - these must not raise exception about missing defaults - for item_key, children_by_key in self.non_gui_children.items(): + for children_by_key in self.non_gui_children.values(): for child_obj in children_by_key.values(): child_obj.set_override_state(state, True) From d60eeb85b4510efa7f6fc7207afa2be1e9c64e54 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 8 Jul 2021 14:58:14 +0200 Subject: [PATCH 328/359] standalone: editorial plugins rename fix gap issue https://github.com/pypeclub/OpenPype/pull/1738#issuecomment-876373865 --- .../{collect_instances.py => collect_editorial_instances.py} | 5 ++++- ..._instance_resources.py => collect_editorial_resources.py} | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) rename openpype/hosts/standalonepublisher/plugins/publish/{collect_instances.py => collect_editorial_instances.py} (98%) rename openpype/hosts/standalonepublisher/plugins/publish/{collect_instance_resources.py => collect_editorial_resources.py} (99%) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py similarity index 98% rename from openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py rename to openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index 0d95da444a..3474cbcdde 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -8,7 +8,7 @@ class CollectInstances(pyblish.api.InstancePlugin): """Collect instances from editorial's OTIO sequence""" order = pyblish.api.CollectorOrder + 0.01 - label = "Collect Instances" + label = "Collect Editorial Instances" hosts = ["standalonepublisher"] families = ["editorial"] @@ -84,6 +84,9 @@ class CollectInstances(pyblish.api.InstancePlugin): if clip.name is None: continue + if isinstance(clip, otio.schema.Gap): + continue + # skip all generators like black ampty if isinstance( clip.media_reference, diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instance_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py similarity index 99% rename from openpype/hosts/standalonepublisher/plugins/publish/collect_instance_resources.py rename to openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py index 565d066fd8..e262009637 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instance_resources.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py @@ -11,7 +11,7 @@ class CollectInstanceResources(pyblish.api.InstancePlugin): # must be after `CollectInstances` order = pyblish.api.CollectorOrder + 0.011 - label = "Collect Instance Resources" + label = "Collect Editorial Resources" hosts = ["standalonepublisher"] families = ["clip"] From f12c117f8669b381ba05dce11ec10c722c323630 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 9 Jul 2021 12:27:23 +0200 Subject: [PATCH 329/359] :window: dont add poetry to path, new installer --- tools/build.ps1 | 11 +++++------ tools/build_win_installer.ps1 | 8 -------- tools/create_env.ps1 | 11 ++++------- tools/create_zip.ps1 | 6 ++---- tools/fetch_thirdparty_libs.ps1 | 7 ++----- tools/make_docs.ps1 | 10 ++++------ tools/run_project_manager.ps1 | 4 ++-- tools/run_settings.ps1 | 4 ++-- tools/run_tests.ps1 | 6 ++---- tools/run_tray.ps1 | 4 ++-- 10 files changed, 25 insertions(+), 46 deletions(-) diff --git a/tools/build.ps1 b/tools/build.ps1 index 89795b0a50..cc4253fe24 100644 --- a/tools/build.ps1 +++ b/tools/build.ps1 @@ -83,7 +83,8 @@ function Show-PSWarning() { function Install-Poetry() { Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Installing Poetry ... " - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - + $env:POETRY_HOME="$openpype_root\.poetry" + (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - } $art = @" @@ -115,11 +116,9 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root @@ -164,7 +163,7 @@ Write-Host " ]" -ForegroundColor white Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -184,7 +183,7 @@ Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Building OpenPype ..." $startTime = [int][double]::Parse((Get-Date -UFormat %s)) -$out = & poetry run python setup.py build 2>&1 +$out = & "$($env:POETRY_HOME)\bin\poetry" run python setup.py build 2>&1 Set-Content -Path "$($openpype_root)\build\build.log" -Value $out if ($LASTEXITCODE -ne 0) { @@ -195,7 +194,7 @@ if ($LASTEXITCODE -ne 0) } Set-Content -Path "$($openpype_root)\build\build.log" -Value $out -& poetry run python "$($openpype_root)\tools\build_dependencies.py" +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\tools\build_dependencies.py" Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "restoring current directory" diff --git a/tools/build_win_installer.ps1 b/tools/build_win_installer.ps1 index 05ec0f9823..a0832e0135 100644 --- a/tools/build_win_installer.ps1 +++ b/tools/build_win_installer.ps1 @@ -64,14 +64,6 @@ function Show-PSWarning() { } } -function Install-Poetry() { - Write-Host ">>> " -NoNewline -ForegroundColor Green - Write-Host "Installing Poetry ... " - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - - # add it to PATH - $env:PATH = "$($env:PATH);$($env:USERPROFILE)\.poetry\bin" -} - $art = @" . . .. . .. diff --git a/tools/create_env.ps1 b/tools/create_env.ps1 index 94a91ce48f..6c8124ccb2 100644 --- a/tools/create_env.ps1 +++ b/tools/create_env.ps1 @@ -49,9 +49,7 @@ function Install-Poetry() { Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Installing Poetry ... " $env:POETRY_HOME="$openpype_root\.poetry" - (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python - - # add it to PATH - $env:PATH = "$($env:PATH);$openpype_root\.poetry\bin" + (Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py -UseBasicParsing).Content | python - } @@ -94,11 +92,10 @@ $current_dir = Get-Location $script_dir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent $openpype_root = (Get-Item $script_dir).parent.FullName -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" + Set-Location -Path $openpype_root @@ -145,7 +142,7 @@ Test-Python Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Install-Poetry Write-Host "INSTALLED" -ForegroundColor Cyan @@ -160,7 +157,7 @@ if (-not (Test-Path -PathType Leaf -Path "$($openpype_root)\poetry.lock")) { Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Installing virtual environment from lock." } -& poetry install --no-root $poetry_verbosity +& "$env:POETRY_HOME\bin\poetry" install --no-root $poetry_verbosity --ansi if ($LASTEXITCODE -ne 0) { Write-Host "!!! " -ForegroundColor yellow -NoNewline Write-Host "Poetry command failed." diff --git a/tools/create_zip.ps1 b/tools/create_zip.ps1 index 1a7520eb11..c27857b480 100644 --- a/tools/create_zip.ps1 +++ b/tools/create_zip.ps1 @@ -45,11 +45,9 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root @@ -87,7 +85,7 @@ if (-not $openpype_version) { Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -107,5 +105,5 @@ Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Generating zip from current sources ..." $env:PYTHONPATH="$($openpype_root);$($env:PYTHONPATH)" $env:OPENPYPE_ROOT="$($openpype_root)" -& poetry run python "$($openpype_root)\tools\create_zip.py" $ARGS +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\tools\create_zip.py" $ARGS Set-Location -Path $current_dir diff --git a/tools/fetch_thirdparty_libs.ps1 b/tools/fetch_thirdparty_libs.ps1 index 23f0b50c7a..16f7b70e7a 100644 --- a/tools/fetch_thirdparty_libs.ps1 +++ b/tools/fetch_thirdparty_libs.ps1 @@ -17,18 +17,15 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root - Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -37,5 +34,5 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { Write-Host "OK" -ForegroundColor Green } -& poetry run python "$($openpype_root)\tools\fetch_thirdparty_libs.py" +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\tools\fetch_thirdparty_libs.py" Set-Location -Path $current_dir diff --git a/tools/make_docs.ps1 b/tools/make_docs.ps1 index 2f9350eff0..45a11171ae 100644 --- a/tools/make_docs.ps1 +++ b/tools/make_docs.ps1 @@ -19,11 +19,9 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root @@ -50,7 +48,7 @@ Write-Host $art -ForegroundColor DarkGreen Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -63,10 +61,10 @@ Write-Host "This will not overwrite existing source rst files, only scan and add Set-Location -Path $openpype_root Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Running apidoc ..." -& poetry run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$($openpype_root)\docs\source" igniter -& poetry run sphinx-apidoc.exe -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$($openpype_root)\docs\source" openpype vendor, openpype\vendor +& "$env:POETRY_HOME\bin\poetry" run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$($openpype_root)\docs\source" igniter +& "$env:POETRY_HOME\bin\poetry" run sphinx-apidoc.exe -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$($openpype_root)\docs\source" openpype vendor, openpype\vendor Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Building html ..." -& poetry run python "$($openpype_root)\setup.py" build_sphinx +& "$env:POETRY_HOME\bin\poetry" run python "$($openpype_root)\setup.py" build_sphinx Set-Location -Path $current_dir diff --git a/tools/run_project_manager.ps1 b/tools/run_project_manager.ps1 index 9886a80316..a9cfbb1e7b 100644 --- a/tools/run_project_manager.ps1 +++ b/tools/run_project_manager.ps1 @@ -47,7 +47,7 @@ Set-Location -Path $openpype_root Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -56,5 +56,5 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { Write-Host "OK" -ForegroundColor Green } -& poetry run python "$($openpype_root)\start.py" projectmanager +& "$env:POETRY_HOME\bin\poetry" run python "$($openpype_root)\start.py" projectmanager Set-Location -Path $current_dir diff --git a/tools/run_settings.ps1 b/tools/run_settings.ps1 index 7477e546b3..1c0aa6e8f3 100644 --- a/tools/run_settings.ps1 +++ b/tools/run_settings.ps1 @@ -27,7 +27,7 @@ Set-Location -Path $openpype_root Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -36,5 +36,5 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { Write-Host "OK" -ForegroundColor Green } -& poetry run python "$($openpype_root)\start.py" settings --dev +& "$env:POETRY_HOME\bin\poetry" run python "$($openpype_root)\start.py" settings --dev Set-Location -Path $current_dir \ No newline at end of file diff --git a/tools/run_tests.ps1 b/tools/run_tests.ps1 index a6882e2a09..e631cb72df 100644 --- a/tools/run_tests.ps1 +++ b/tools/run_tests.ps1 @@ -59,11 +59,9 @@ $openpype_root = (Get-Item $script_dir).parent.FullName $env:_INSIDE_OPENPYPE_TOOL = "1" -# make sure Poetry is in PATH if (-not (Test-Path 'env:POETRY_HOME')) { $env:POETRY_HOME = "$openpype_root\.poetry" } -$env:PATH = "$($env:PATH);$($env:POETRY_HOME)\bin" Set-Location -Path $openpype_root @@ -83,7 +81,7 @@ Write-Host " ] ..." -ForegroundColor white Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -102,7 +100,7 @@ Write-Host ">>> " -NoNewline -ForegroundColor green Write-Host "Testing OpenPype ..." $original_pythonpath = $env:PYTHONPATH $env:PYTHONPATH="$($openpype_root);$($env:PYTHONPATH)" -& poetry run pytest -x --capture=sys --print -W ignore::DeprecationWarning "$($openpype_root)/tests" +& "$env:POETRY_HOME\bin\poetry" run pytest -x --capture=sys --print -W ignore::DeprecationWarning "$($openpype_root)/tests" $env:PYTHONPATH = $original_pythonpath Write-Host ">>> " -NoNewline -ForegroundColor green diff --git a/tools/run_tray.ps1 b/tools/run_tray.ps1 index 533a791836..872c1524a6 100644 --- a/tools/run_tray.ps1 +++ b/tools/run_tray.ps1 @@ -26,7 +26,7 @@ Set-Location -Path $openpype_root Write-Host ">>> " -NoNewline -ForegroundColor Green Write-Host "Reading Poetry ... " -NoNewline -if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { +if (-not (Test-Path -PathType Container -Path "$($env:POETRY_HOME)\bin")) { Write-Host "NOT FOUND" -ForegroundColor Yellow Write-Host "*** " -NoNewline -ForegroundColor Yellow Write-Host "We need to install Poetry create virtual env first ..." @@ -35,5 +35,5 @@ if (-not (Test-Path -PathType Container -Path "$openpype_root\.poetry\bin")) { Write-Host "OK" -ForegroundColor Green } -& poetry run python "$($openpype_root)\start.py" tray --debug +& "$($env:POETRY_HOME)\bin\poetry" run python "$($openpype_root)\start.py" tray --debug Set-Location -Path $current_dir \ No newline at end of file From 43f7f8276aa714a465e9970a8cd8430b38772691 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 9 Jul 2021 12:38:19 +0200 Subject: [PATCH 330/359] =?UTF-8?q?=F0=9F=90=A7=F0=9F=8D=8E=20don't=20add?= =?UTF-8?q?=20poetry=20to=20path,=20new=20installer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/build.sh | 23 +++-------------------- tools/create_env.sh | 16 +++++++--------- tools/create_zip.sh | 4 +--- tools/fetch_thirdparty_libs.sh | 7 +------ tools/make_docs.sh | 8 +++----- tools/run_projectmanager.sh | 4 +--- tools/run_settings.sh | 4 +--- tools/run_tests.sh | 4 +--- tools/run_tray.sh | 4 +--- 9 files changed, 19 insertions(+), 55 deletions(-) diff --git a/tools/build.sh b/tools/build.sh index aa8f0121ea..c44e7157af 100755 --- a/tools/build.sh +++ b/tools/build.sh @@ -140,21 +140,6 @@ realpath () { echo $(cd $(dirname "$1") || return; pwd)/$(basename "$1") } -############################################################################## -# Install Poetry when needed -# Globals: -# PATH -# Arguments: -# None -# Returns: -# None -############################################################################### -install_poetry () { - echo -e "${BIGreen}>>>${RST} Installing Poetry ..." - command -v curl >/dev/null 2>&1 || { echo -e "${BIRed}!!!${RST}${BIYellow} Missing ${RST}${BIBlue}curl${BIYellow} command.${RST}"; return 1; } - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - -} - # Main main () { echo -e "${BGreen}" @@ -171,11 +156,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIYellow}---${RST} Cleaning build directory ..." rm -rf "$openpype_root/build" && mkdir "$openpype_root/build" > /dev/null @@ -201,11 +184,11 @@ if [ "$disable_submodule_update" == 1 ]; then fi echo -e "${BIGreen}>>>${RST} Building ..." if [[ "$OSTYPE" == "linux-gnu"* ]]; then - poetry run python "$openpype_root/setup.py" build > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } + "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" build > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } elif [[ "$OSTYPE" == "darwin"* ]]; then - poetry run python "$openpype_root/setup.py" bdist_mac > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } + "$POETRY_HOME/bin/poetry" run python "$openpype_root/setup.py" bdist_mac > "$openpype_root/build/build.log" || { echo -e "${BIRed}!!!${RST} Build failed, see the build log."; return; } fi - poetry run python "$openpype_root/tools/build_dependencies.py" + "$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/build_dependencies.py" if [[ "$OSTYPE" == "darwin"* ]]; then # fix code signing issue diff --git a/tools/create_env.sh b/tools/create_env.sh index 226a26e199..cc9eddc317 100755 --- a/tools/create_env.sh +++ b/tools/create_env.sh @@ -109,8 +109,7 @@ install_poetry () { echo -e "${BIGreen}>>>${RST} Installing Poetry ..." export POETRY_HOME="$openpype_root/.poetry" command -v curl >/dev/null 2>&1 || { echo -e "${BIRed}!!!${RST}${BIYellow} Missing ${RST}${BIBlue}curl${BIYellow} command.${RST}"; return 1; } - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - - export PATH="$PATH:$POETRY_HOME/bin" + curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python - } ############################################################################## @@ -154,11 +153,10 @@ main () { # Directories openpype_root=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}"))) - # make sure Poetry is in PATH + if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" pushd "$openpype_root" > /dev/null || return > /dev/null @@ -177,7 +175,7 @@ main () { echo -e "${BIGreen}>>>${RST} Installing dependencies ..." fi - poetry install --no-root $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return; } + "$POETRY_HOME/bin/poetry" install --no-root $poetry_verbosity || { echo -e "${BIRed}!!!${RST} Poetry environment installation failed"; return; } echo -e "${BIGreen}>>>${RST} Cleaning cache files ..." clean_pyc @@ -186,10 +184,10 @@ main () { # cx_freeze will crash on missing __pychache__ on these but # reinstalling them solves the problem. echo -e "${BIGreen}>>>${RST} Fixing pycache bug ..." - poetry run python -m pip install --force-reinstall pip - poetry run pip install --force-reinstall setuptools - poetry run pip install --force-reinstall wheel - poetry run python -m pip install --force-reinstall pip + "$POETRY_HOME/bin/poetry" run python -m pip install --force-reinstall pip + "$POETRY_HOME/bin/poetry" run pip install --force-reinstall setuptools + "$POETRY_HOME/bin/poetry" run pip install --force-reinstall wheel + "$POETRY_HOME/bin/poetry" run python -m pip install --force-reinstall pip } main -3 diff --git a/tools/create_zip.sh b/tools/create_zip.sh index ec0276b040..85ee18a839 100755 --- a/tools/create_zip.sh +++ b/tools/create_zip.sh @@ -114,11 +114,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" pushd "$openpype_root" > /dev/null || return > /dev/null @@ -134,7 +132,7 @@ main () { echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..." PYTHONPATH="$openpype_root:$PYTHONPATH" OPENPYPE_ROOT="$openpype_root" - poetry run python3 "$openpype_root/tools/create_zip.py" "$@" + "$POETRY_HOME/bin/poetry" run python3 "$openpype_root/tools/create_zip.py" "$@" } main "$@" diff --git a/tools/fetch_thirdparty_libs.sh b/tools/fetch_thirdparty_libs.sh index 31f109ba68..93d0674965 100755 --- a/tools/fetch_thirdparty_libs.sh +++ b/tools/fetch_thirdparty_libs.sh @@ -1,8 +1,5 @@ #!/usr/bin/env bash -# Run Pype Tray - - art () { cat <<-EOF @@ -82,11 +79,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" if [ -f "$POETRY_HOME/bin/poetry" ]; then @@ -100,7 +95,7 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null echo -e "${BIGreen}>>>${RST} Running Pype tool ..." - poetry run python "$openpype_root/tools/fetch_thirdparty_libs.py" + "$POETRY_HOME/bin/poetry" run python "$openpype_root/tools/fetch_thirdparty_libs.py" } main \ No newline at end of file diff --git a/tools/make_docs.sh b/tools/make_docs.sh index 9dfab26a38..52ee57dcf0 100755 --- a/tools/make_docs.sh +++ b/tools/make_docs.sh @@ -83,11 +83,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" if [ -f "$POETRY_HOME/bin/poetry" ]; then @@ -101,11 +99,11 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null echo -e "${BIGreen}>>>${RST} Running apidoc ..." - poetry run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$openpype_root/docs/source" igniter - poetry run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$openpype_root/docs/source" openpype vendor, openpype\vendor + "$POETRY_HOME/bin/poetry" run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$openpype_root/docs/source" igniter + "$POETRY_HOME/bin/poetry" run sphinx-apidoc -M -e -d 10 --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode -o "$openpype_root/docs/source" openpype vendor, openpype\vendor echo -e "${BIGreen}>>>${RST} Building html ..." - poetry run python3 "$openpype_root/setup.py" build_sphinx + "$POETRY_HOME/bin/poetry" run python3 "$openpype_root/setup.py" build_sphinx } main diff --git a/tools/run_projectmanager.sh b/tools/run_projectmanager.sh index 312f321d67..b5c858c34a 100755 --- a/tools/run_projectmanager.sh +++ b/tools/run_projectmanager.sh @@ -79,11 +79,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" pushd "$openpype_root" > /dev/null || return > /dev/null @@ -97,7 +95,7 @@ main () { fi echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..." - poetry run python "$openpype_root/start.py" projectmanager + "$POETRY_HOME/bin/poetry" run python "$openpype_root/start.py" projectmanager } main diff --git a/tools/run_settings.sh b/tools/run_settings.sh index 0287043bb6..5a465dce2c 100755 --- a/tools/run_settings.sh +++ b/tools/run_settings.sh @@ -79,11 +79,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" pushd "$openpype_root" > /dev/null || return > /dev/null @@ -97,7 +95,7 @@ main () { fi echo -e "${BIGreen}>>>${RST} Generating zip from current sources ..." - poetry run python3 "$openpype_root/start.py" settings --dev + "$POETRY_HOME/bin/poetry" run python3 "$openpype_root/start.py" settings --dev } main diff --git a/tools/run_tests.sh b/tools/run_tests.sh index 90977edc83..8f8f82fd9c 100755 --- a/tools/run_tests.sh +++ b/tools/run_tests.sh @@ -98,11 +98,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" if [ -f "$POETRY_HOME/bin/poetry" ]; then @@ -118,7 +116,7 @@ main () { echo -e "${BIGreen}>>>${RST} Testing OpenPype ..." original_pythonpath=$PYTHONPATH export PYTHONPATH="$openpype_root:$PYTHONPATH" - poetry run pytest -x --capture=sys --print -W ignore::DeprecationWarning "$openpype_root/tests" + "$POETRY_HOME/bin/poetry" run pytest -x --capture=sys --print -W ignore::DeprecationWarning "$openpype_root/tests" PYTHONPATH=$original_pythonpath } diff --git a/tools/run_tray.sh b/tools/run_tray.sh index 339ff6f918..2eb9886063 100755 --- a/tools/run_tray.sh +++ b/tools/run_tray.sh @@ -56,11 +56,9 @@ main () { _inside_openpype_tool="1" - # make sure Poetry is in PATH if [[ -z $POETRY_HOME ]]; then export POETRY_HOME="$openpype_root/.poetry" fi - export PATH="$POETRY_HOME/bin:$PATH" echo -e "${BIGreen}>>>${RST} Reading Poetry ... \c" if [ -f "$POETRY_HOME/bin/poetry" ]; then @@ -74,7 +72,7 @@ main () { pushd "$openpype_root" > /dev/null || return > /dev/null echo -e "${BIGreen}>>>${RST} Running OpenPype Tray with debug option ..." - poetry run python3 "$openpype_root/start.py" tray --debug + "$POETRY_HOME/bin/poetry" run python3 "$openpype_root/start.py" tray --debug } main \ No newline at end of file From a7d5c63228c2e3b56a16fa416653953f4e367237 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 9 Jul 2021 12:50:16 +0200 Subject: [PATCH 331/359] =?UTF-8?q?=F0=9F=90=9E:=20fix=20yeti=20settings?= =?UTF-8?q?=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/maya/plugins/publish/extract_yeti_rig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index b9bed47fa5..eef3c4e9af 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -192,12 +192,12 @@ class ExtractYetiRig(openpype.api.Extractor): 'stagingDir': dirname } ) - self.log.info("settings file: {}".format(settings)) + self.log.info("settings file: {}".format(settings_path)) instance.data["representations"].append( { 'name': 'rigsettings', 'ext': 'rigsettings', - 'files': os.path.basename(settings), + 'files': os.path.basename(settings_path), 'stagingDir': dirname } ) From b7882ba8337f1afc09a569636a42bb5e6257210b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 9 Jul 2021 15:29:06 +0200 Subject: [PATCH 332/359] Fix - added better validation and documentation --- .../publish/validate_instance_asset.py | 61 +++++++++++++++++++ .../publish/validate_instance_asset.py | 24 +++++--- 2 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py new file mode 100644 index 0000000000..eff89adcb3 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py @@ -0,0 +1,61 @@ +from avalon import api +import pyblish.api +import openpype.api +from avalon import aftereffects + + +class ValidateInstanceAssetRepair(pyblish.api.Action): + """Repair the instance asset with value from Context.""" + + 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) + stub = aftereffects.stub() + for instance in instances: + data = stub.read(instance[0]) + + data["asset"] = api.Session["AVALON_ASSET"] + stub.imprint(instance[0], data) + + +class ValidateInstanceAsset(pyblish.api.InstancePlugin): + """Validate the instance asset is the current selected context asset. + + As it might happen that multiple worfiles are opened at same time, + switching between them would mess with selected context. (From Launcher + or Ftrack). + + In that case outputs might be output under wrong asset! + + Repair action will use Context asset value (from Workfiles or Launcher) + Closing and reopening with Workfiles will refresh Context value. + """ + + label = "Validate Instance Asset" + hosts = ["aftereffects"] + actions = [ValidateInstanceAssetRepair] + order = openpype.api.ValidateContentsOrder + + def process(self, instance): + instance_asset = instance.data["asset"] + current_asset = api.Session["AVALON_ASSET"] + msg = ( + f"Instance asset {instance_asset} is not the same " + f"as current context {current_asset}. PLEASE DO:\n" + f"Repair with 'A' action to use '{current_asset}'.\n" + f"If that's not correct value, close workfile and " + f"reopen via Workfiles!" + ) + assert instance_asset == current_asset, msg diff --git a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py index a1de02f319..4dc1972074 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -1,5 +1,4 @@ -import os - +from avalon import api import pyblish.api import openpype.api from avalon import photoshop @@ -27,12 +26,20 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): for instance in instances: data = stub.read(instance[0]) - data["asset"] = os.environ["AVALON_ASSET"] + data["asset"] = api.Session["AVALON_ASSET"] stub.imprint(instance[0], data) class ValidateInstanceAsset(pyblish.api.InstancePlugin): - """Validate the instance asset is the current asset.""" + """Validate the instance asset is the current selected context asset. + + As it might happen that multiple worfiles are opened, switching + between them would mess with selected context. + In that case outputs might be output under wrong asset! + + Repair action will use Context asset value (from Workfiles or Launcher) + Closing and reopening with Workfiles will refresh Context value. + """ label = "Validate Instance Asset" hosts = ["photoshop"] @@ -41,9 +48,12 @@ class ValidateInstanceAsset(pyblish.api.InstancePlugin): def process(self, instance): instance_asset = instance.data["asset"] - current_asset = os.environ["AVALON_ASSET"] + current_asset = api.Session["AVALON_ASSET"] msg = ( - "Instance asset is not the same as current asset:" - f"\nInstance: {instance_asset}\nCurrent: {current_asset}" + f"Instance asset {instance_asset} is not the same " + f"as current context {current_asset}. PLEASE DO:\n" + f"Repair with 'A' action to use '{current_asset}'.\n" + f"If that's not correct value, close workfile and " + f"reopen via Workfiles!" ) assert instance_asset == current_asset, msg From 39efe7e105b9d6d41cb9e16902086a7f97d09838 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 10 Jul 2021 03:41:06 +0000 Subject: [PATCH 333/359] [Automated] Bump version --- CHANGELOG.md | 57 +++++++++++++++------------------------------ openpype/version.py | 2 +- 2 files changed, 20 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e658d6995..5e76d7b76a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,16 @@ # Changelog -## [3.2.0-nightly.6](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) **🚀 Enhancements** +- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) +- Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799) +- Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) +- Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) - Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776) -- Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769) - Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766) - Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) @@ -16,18 +19,18 @@ - Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739) - Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736) - PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726) -- Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725) -- Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717) - Toggle Ftrack upload in StandalonePublisher [\#1708](https://github.com/pypeclub/OpenPype/pull/1708) -- Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701) -- Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699) -- Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695) **🐛 Bug fixes** +- nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) +- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) +- Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) +- Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) - Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) - FFprobe streams order [\#1775](https://github.com/pypeclub/OpenPype/pull/1775) - Fix - single file files are str only, cast it to list to count properly [\#1772](https://github.com/pypeclub/OpenPype/pull/1772) +- Environments in app executable for MacOS [\#1768](https://github.com/pypeclub/OpenPype/pull/1768) - Project specific environments [\#1767](https://github.com/pypeclub/OpenPype/pull/1767) - Settings UI with refresh button [\#1764](https://github.com/pypeclub/OpenPype/pull/1764) - Standalone publisher thumbnail extractor fix [\#1761](https://github.com/pypeclub/OpenPype/pull/1761) @@ -36,22 +39,18 @@ - hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743) - Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742) - Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741) +- StandalonePublisher: failing collector for editorial [\#1738](https://github.com/pypeclub/OpenPype/pull/1738) - Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) - TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735) -- Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734) -- Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733) -- Ftrack sync status [\#1732](https://github.com/pypeclub/OpenPype/pull/1732) -- TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724) -- Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716) -- Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714) -- Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711) - Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) +- Unreal: launching on Linux [\#1672](https://github.com/pypeclub/OpenPype/pull/1672) **Merged pull requests:** +- Build: don't add Poetry to `PATH` [\#1808](https://github.com/pypeclub/OpenPype/pull/1808) - Bump prismjs from 1.23.0 to 1.24.0 in /website [\#1773](https://github.com/pypeclub/OpenPype/pull/1773) +- Bc/fix/docs [\#1771](https://github.com/pypeclub/OpenPype/pull/1771) - TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755) -- Sync main 2.x back to 2.x develop [\#1715](https://github.com/pypeclub/OpenPype/pull/1715) ## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24) @@ -66,26 +65,21 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3) -**🚀 Enhancements** - -- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - **🐛 Bug fixes** - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) +**Merged pull requests:** + +- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) + ## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2) -**🚀 Enhancements** - -- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650) - **🐛 Bug fixes** - Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713) -- StandalonePublisher: instance data attribute `keepSequence` [\#1668](https://github.com/pypeclub/OpenPype/pull/1668) **Merged pull requests:** @@ -97,19 +91,12 @@ **🚀 Enhancements** +- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703) - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) - OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) - Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) - \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683) - Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682) -- Project Manger: Default name column width [\#1669](https://github.com/pypeclub/OpenPype/pull/1669) -- Remove outline in stylesheet [\#1667](https://github.com/pypeclub/OpenPype/pull/1667) -- TVPaint: Creator take layer name as default value for subset variant [\#1663](https://github.com/pypeclub/OpenPype/pull/1663) -- TVPaint custom subset template [\#1662](https://github.com/pypeclub/OpenPype/pull/1662) -- Editorial: conform assets validator [\#1659](https://github.com/pypeclub/OpenPype/pull/1659) -- Feature Slack integration [\#1657](https://github.com/pypeclub/OpenPype/pull/1657) -- Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653) -- \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649) **🐛 Bug fixes** @@ -119,15 +106,9 @@ - Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) - Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) -- Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660) -- Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652) -- Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648) -- Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646) -- Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645) **Merged pull requests:** -- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) - Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) diff --git a/openpype/version.py b/openpype/version.py index 86d62a83d0..dabeacc084 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.6" +__version__ = "3.2.0-nightly.7" From 500a2548035a00e82b814ee297f46a6885b89f72 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:36:20 +0200 Subject: [PATCH 334/359] rawjson entity can store value as string --- openpype/settings/entities/input_entities.py | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 2abb7a2253..6952529963 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -1,5 +1,6 @@ import re import copy +import json from abc import abstractmethod from .base_entity import ItemEntity @@ -440,6 +441,7 @@ class RawJsonEntity(InputEntity): def _item_initalization(self): # Schema must define if valid value is dict or list + store_as_string = self.schema_data.get("store_as_string", False) is_list = self.schema_data.get("is_list", False) if is_list: valid_value_types = (list, ) @@ -448,6 +450,8 @@ class RawJsonEntity(InputEntity): valid_value_types = (dict, ) value_on_not_set = {} + self.store_as_string = store_as_string + self._is_list = is_list self.valid_value_types = valid_value_types self.value_on_not_set = value_on_not_set @@ -491,6 +495,23 @@ class RawJsonEntity(InputEntity): result = self.metadata != self._metadata_for_current_state() return result + def schema_validations(self): + if self.store_as_string and self.is_env_group: + reason = ( + "RawJson entity can't store environment group metadata" + " as string." + ) + raise EntitySchemaError(self, reason) + super(RawJsonEntity, self).schema_validations() + + def _convert_to_valid_type(self, value): + if isinstance(value, STRING_TYPE): + try: + return json.loads(value) + except Exception: + pass + return super(RawJsonEntity, self)._convert_to_valid_type(value) + def _metadata_for_current_state(self): if ( self._override_state is OverrideState.PROJECT @@ -510,6 +531,9 @@ class RawJsonEntity(InputEntity): value = super(RawJsonEntity, self)._settings_value() if self.is_env_group and isinstance(value, dict): value.update(self.metadata) + + if self.store_as_string: + return json.dumps(value) return value def _prepare_value(self, value): From 27bfa50da6d6c8fa1c566e89f3ba250d26aff6d0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:36:35 +0200 Subject: [PATCH 335/359] store project folder structure as text --- .../schemas/projects_schema/schema_project_global.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json index 6e5cf0671c..a8bce47592 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_global.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_global.json @@ -17,7 +17,8 @@ "type": "raw-json", "label": "Project Folder Structure", "key": "project_folder_structure", - "use_label_wrap": true + "use_label_wrap": true, + "store_as_string": true }, { "type": "schema", From c55d67bb58e00a01eeaf885e935c03f1869163fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:36:51 +0200 Subject: [PATCH 336/359] action where project_folder_structure is used expect string value --- .../event_handlers_user/action_create_project_structure.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py index d7ac866e42..035a1c60de 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py @@ -1,5 +1,6 @@ import os import re +import json from openpype.modules.ftrack.lib import BaseAction, statics_icon from openpype.api import Anatomy, get_project_settings @@ -84,6 +85,9 @@ class CreateProjectFolders(BaseAction): } try: + if isinstance(project_folder_structure, str): + project_folder_structure = json.loads(project_folder_structure) + # Get paths based on presets basic_paths = self.get_path_items(project_folder_structure) self.create_folders(basic_paths, project_entity) From 62782b4db6361ec18632400c4b322412767f5b14 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:40:09 +0200 Subject: [PATCH 337/359] resaved defaults --- .../defaults/project_settings/global.json | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 037fa63a29..6771dfabf8 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -271,28 +271,7 @@ } } }, - "project_folder_structure": { - "__project_root__": { - "prod": {}, - "resources": { - "footage": { - "plates": {}, - "offline": {} - }, - "audio": {}, - "art_dept": {} - }, - "editorial": {}, - "assets[ftrack.Library]": { - "characters[ftrack]": {}, - "locations[ftrack]": {} - }, - "shots[ftrack.Sequence]": { - "scripts": {}, - "editorial[ftrack.Folder]": {} - } - } - }, + "project_folder_structure": "{\"__project_root__\": {\"prod\": {}, \"resources\": {\"footage\": {\"plates\": {}, \"offline\": {}}, \"audio\": {}, \"art_dept\": {}}, \"editorial\": {}, \"assets[ftrack.Library]\": {\"characters[ftrack]\": {}, \"locations[ftrack]\": {}}, \"shots[ftrack.Sequence]\": {\"scripts\": {}, \"editorial[ftrack.Folder]\": {}}}}", "sync_server": { "enabled": true, "config": { From 470b3d4add0cce3592c9ec3cd688cf818698c489 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 12 Jul 2021 11:43:17 +0200 Subject: [PATCH 338/359] added store_as_string to readme --- openpype/settings/entities/schemas/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 3c360b892f..d457e44e74 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -337,6 +337,11 @@ How output of the schema could look like on save: - schema also defines valid value type - by default it is dictionary - to be able use list it is required to define `is_list` to `true` +- output can be stored as string + - this is to allow any keys in dictionary + - set key `store_as_string` to `true` + - code using that setting must expected that value is string and use json module to convert it to python types + ``` { "type": "raw-json", From 6e0a51fa7c39a491668bd4e04195ef5a7d7f8154 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jul 2021 12:28:05 +0200 Subject: [PATCH 339/359] remove unnecessary if --- openpype/hosts/maya/plugins/publish/extract_yeti_rig.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index eef3c4e9af..56d5dfe901 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -133,10 +133,10 @@ class ExtractYetiRig(openpype.api.Extractor): image_search_path = resources_dir = instance.data["resourcesDir"] settings = instance.data.get("rigsettings", None) - if settings: - settings["imageSearchPath"] = image_search_path - with open(settings_path, "w") as fp: - json.dump(settings, fp, ensure_ascii=False) + assert settings, "Yeti rig settings were not collected." + settings["imageSearchPath"] = image_search_path + with open(settings_path, "w") as fp: + json.dump(settings, fp, ensure_ascii=False) # add textures to transfers if 'transfers' not in instance.data: From 4b2aba2f450dfc1a9a46142d8b6df441ac8bd2f6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jul 2021 12:59:19 +0200 Subject: [PATCH 340/359] change settings retrieval --- .../plugins/publish/submit_maya_deadline.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 0e09641200..c0c39a52b6 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -272,18 +272,19 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): ) self._job_info = ( - context.data["project_settings"] - ["maya"] - ["publish"] - ["deadline"] - ["jobInfo"] + context.data["project_settings"].get( + "maya", {}).get( + "publish", {}).get( + "deadline", {}).get( + "jobInfo", {}) ) + self._plugin_info = ( - context.data["project_settings"] - ["maya"] - ["publish"] - ["deadline"] - ["pluginInfo"] + context.data["project_settings"].get( + "maya", {}).get( + "publish", {}).get( + "deadline", {}).get( + "pluginInfo", {}) ) assert self._deadline_url, "Requires DEADLINE_REST_URL" From 0551d76fb6ad74e38de35ada406e41c1acb614c7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jul 2021 13:16:38 +0200 Subject: [PATCH 341/359] move settings to deadline --- .../plugins/publish/submit_maya_deadline.py | 8 ++++---- .../defaults/project_settings/deadline.json | 14 ++++++++++---- .../defaults/project_settings/maya.json | 4 ---- .../schema_project_deadline.json | 10 ++++++++++ .../schemas/schema_maya_publish.json | 18 ------------------ 5 files changed, 24 insertions(+), 30 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index c0c39a52b6..5cb6dbbd88 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -273,17 +273,17 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self._job_info = ( context.data["project_settings"].get( - "maya", {}).get( - "publish", {}).get( "deadline", {}).get( + "publish", {}).get( + "MayaSubmitDeadline", {}).get( "jobInfo", {}) ) self._plugin_info = ( context.data["project_settings"].get( - "maya", {}).get( - "publish", {}).get( "deadline", {}).get( + "publish", {}).get( + "MayaSubmitDeadline", {}).get( "pluginInfo", {}) ) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 5861015f2c..2dba20d63c 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -3,9 +3,13 @@ "ValidateExpectedFiles": { "enabled": true, "active": true, - "families": ["render"], - "targets": ["deadline"], - "allow_user_override": true + "allow_user_override": true, + "families": [ + "render" + ], + "targets": [ + "deadline" + ] }, "MayaSubmitDeadline": { "enabled": true, @@ -15,7 +19,9 @@ "use_published": true, "asset_dependencies": true, "group": "none", - "limit": [] + "limit": [], + "jobInfo": {}, + "pluginInfo": {} }, "NukeSubmitDeadline": { "enabled": true, diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 62d8a74670..284a1a0040 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -391,10 +391,6 @@ } } }, - "deadline": { - "jobInfo": {}, - "pluginInfo": {} - }, "ExtractCameraAlembic": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 3281c9ce4d..27eeaef559 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -108,6 +108,16 @@ "key": "limit", "label": "Limit Groups", "object_type": "text" + }, + { + "type": "raw-json", + "key": "jobInfo", + "label": "Additional JobInfo data" + }, + { + "type": "raw-json", + "key": "pluginInfo", + "label": "Additional PluginInfo data" } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 7e50682f5d..5ca7059ee5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -309,24 +309,6 @@ "type": "schema", "name": "schema_maya_capture" }, - { - "type": "dict", - "collapsible": true, - "key": "deadline", - "label": "Additional Deadline Settings", - "children": [ - { - "type": "raw-json", - "key": "jobInfo", - "label": "Additional JobInfo data" - }, - { - "type": "raw-json", - "key": "pluginInfo", - "label": "Additional PluginInfo data" - } - ] - }, { "type": "dict", "collapsible": true, From 4d96cdc7200808c6accbb190e4ffea3f59a74cd6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 12 Jul 2021 13:34:19 +0200 Subject: [PATCH 342/359] fix check for Group defaults --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 5cb6dbbd88..a652da7786 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -423,7 +423,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.payload_skeleton["JobInfo"]["Priority"] = \ self._instance.data.get("priority", 50) - if self.group != "none": + if self.group != "none" and self.group: self.payload_skeleton["JobInfo"]["Group"] = self.group if self.limit_groups: From 0eaf60f358598422cc6067444b5bf139584874cc Mon Sep 17 00:00:00 2001 From: OpenPype Date: Tue, 13 Jul 2021 11:54:36 +0000 Subject: [PATCH 343/359] [Automated] Release --- CHANGELOG.md | 7 +++---- openpype/version.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e76d7b76a..bc659bd629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Changelog -## [3.2.0-nightly.7](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...3.2.0) **🚀 Enhancements** @@ -11,6 +11,7 @@ - Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) - Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) - Settings application use python 2 only where needed [\#1776](https://github.com/pypeclub/OpenPype/pull/1776) +- Settings UI copy/paste [\#1769](https://github.com/pypeclub/OpenPype/pull/1769) - Workfile tool widths [\#1766](https://github.com/pypeclub/OpenPype/pull/1766) - Push hierarchical attributes care about task parent changes [\#1763](https://github.com/pypeclub/OpenPype/pull/1763) - Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757) @@ -24,7 +25,6 @@ **🐛 Bug fixes** - nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) -- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) - Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) - Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) - Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) @@ -105,7 +105,6 @@ - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) - Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) - Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) -- Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index dabeacc084..7bcd7face2 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0-nightly.7" +__version__ = "3.2.0" From c8fe973155beb7323184ee6b31476257467b609f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:44:39 +0200 Subject: [PATCH 344/359] standalone: plugins prepare for settings --- .../publish/collect_editorial_instances.py | 17 +++++++----- .../publish/collect_editorial_resources.py | 20 ++++++++------ .../plugins/publish/collect_hierarchy.py | 27 ++++++++++--------- 3 files changed, 37 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py index 3474cbcdde..dbf2574a9d 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_instances.py @@ -17,16 +17,12 @@ class CollectInstances(pyblish.api.InstancePlugin): "referenceMain": { "family": "review", "families": ["clip"], - "extensions": [".mp4"] + "extensions": ["mp4"] }, "audioMain": { "family": "audio", "families": ["clip"], - "extensions": [".wav"], - }, - "shotMain": { - "family": "shot", - "families": [] + "extensions": ["wav"], } } timeline_frame_start = 900000 # starndard edl default (10:00:00:00) @@ -178,7 +174,16 @@ class CollectInstances(pyblish.api.InstancePlugin): data_key: instance.data.get(data_key)}) # adding subsets to context as instances + self.subsets.update({ + "shotMain": { + "family": "shot", + "families": [] + } + }) for subset, properities in self.subsets.items(): + if properities["version"] == 0: + properities.pop("version") + # adding Review-able instance subset_instance_data = instance_data.copy() subset_instance_data.update(properities) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py index e262009637..ffa24cfd93 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial_resources.py @@ -177,19 +177,23 @@ class CollectInstanceResources(pyblish.api.InstancePlugin): collection_head_name = None # loop trough collections and create representations for _collection in collections: - ext = _collection.tail + ext = _collection.tail[1:] collection_head_name = _collection.head frame_start = list(_collection.indexes)[0] frame_end = list(_collection.indexes)[-1] repre_data = { "frameStart": frame_start, "frameEnd": frame_end, - "name": ext[1:], - "ext": ext[1:], + "name": ext, + "ext": ext, "files": [item for item in _collection], "stagingDir": staging_dir } + if instance_data.get("keepSequence"): + repre_data_keep = deepcopy(repre_data) + instance_data["representations"].append(repre_data_keep) + if "review" in instance_data["families"]: repre_data.update({ "thumbnail": True, @@ -208,20 +212,20 @@ class CollectInstanceResources(pyblish.api.InstancePlugin): # loop trough reminders and create representations for _reminding_file in remainder: - ext = os.path.splitext(_reminding_file)[-1] + ext = os.path.splitext(_reminding_file)[-1][1:] if ext not in instance_data["extensions"]: continue if collection_head_name and ( - (collection_head_name + ext[1:]) not in _reminding_file - ) and (ext in [".mp4", ".mov"]): + (collection_head_name + ext) not in _reminding_file + ) and (ext in ["mp4", "mov"]): self.log.info(f"Skipping file: {_reminding_file}") continue frame_start = 1 frame_end = 1 repre_data = { - "name": ext[1:], - "ext": ext[1:], + "name": ext, + "ext": ext, "files": _reminding_file, "stagingDir": staging_dir } diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py index be36f30f4b..ba2aed4bfc 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_hierarchy.py @@ -131,20 +131,21 @@ class CollectHierarchyInstance(pyblish.api.ContextPlugin): tasks_to_add = dict() project_tasks = io.find_one({"type": "project"})["config"]["tasks"] for task_name, task_data in self.shot_add_tasks.items(): - try: - if task_data["type"] in project_tasks.keys(): - tasks_to_add.update({task_name: task_data}) - else: - raise KeyError( - "Wrong FtrackTaskType `{}` for `{}` is not" - " existing in `{}``".format( - task_data["type"], - task_name, - list(project_tasks.keys()))) - except KeyError as error: + _task_data = deepcopy(task_data) + + # fixing enumerator from settings + _task_data["type"] = task_data["type"][0] + + # check if task type in project task types + if _task_data["type"] in project_tasks.keys(): + tasks_to_add.update({task_name: _task_data}) + else: raise KeyError( - "Wrong presets: `{0}`".format(error) - ) + "Wrong FtrackTaskType `{}` for `{}` is not" + " existing in `{}``".format( + _task_data["type"], + task_name, + list(project_tasks.keys()))) instance.data["tasks"] = tasks_to_add else: From 7062096ef70d9589bae2ca7a4eb7243cdbf3b412 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:45:03 +0200 Subject: [PATCH 345/359] standalone: plugin settings --- .../project_settings/standalonepublisher.json | 45 ++++++ .../schema_project_standalonepublisher.json | 139 ++++++++++++++++++ 2 files changed, 184 insertions(+) diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 443203951d..52020f2ce8 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -165,6 +165,51 @@ ], "output": [] } + }, + "CollectHierarchyInstance": { + "shot_rename_template": "{project[code]}_{_sequence_}_{_shot_}", + "shot_rename_search_patterns": { + "_sequence_": "(\\d{4})(?=_\\d{4})", + "_shot_": "(\\d{4})(?!_\\d{4})" + }, + "shot_add_hierarchy": { + "parents_path": "{project}/{folder}/{sequence}", + "parents": { + "project": "{project[name]}", + "sequence": "{_sequence_}", + "folder": "shots" + } + }, + "shot_add_tasks": {} + }, + "shot_add_tasks": { + "custom_start_frame": 0, + "timeline_frame_start": 900000, + "timeline_frame_offset": 0, + "subsets": { + "referenceMain": { + "family": "review", + "families": [ + "clip" + ], + "extensions": [ + "mp4" + ], + "version": 0, + "keepSequence": false + }, + "audioMain": { + "family": "audio", + "families": [ + "clip" + ], + "extensions": [ + "wav" + ], + "version": 0, + "keepSequence": false + } + } } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 0ef7612805..30144341c2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -130,6 +130,145 @@ ] } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CollectHierarchyInstance", + "label": "Collect Instance Hierarchy", + "is_group": true, + "children": [ + { + "type": "text", + "key": "shot_rename_template", + "label": "Shot rename template" + }, + { + "key": "shot_rename_search_patterns", + "label": "Shot renaming paterns search", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "text" + } + }, + { + "type": "dict", + "key": "shot_add_hierarchy", + "label": "Shot hierarchy", + "children": [ + { + "type": "text", + "key": "parents_path", + "label": "Parents path template" + }, + { + "key": "parents", + "label": "Parents", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "text" + } + } + ] + }, + { + "key": "shot_add_tasks", + "label": "Add tasks to shot", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "task-types-enum", + "key": "type", + "label": "Task type" + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "shot_add_tasks", + "label": "Collect Clip Instances", + "is_group": true, + "children": [ + { + "type": "number", + "key": "custom_start_frame", + "label": "Custom start frame", + "default": 0, + "minimum": 1, + "maximum": 100000 + }, + { + "type": "number", + "key": "timeline_frame_start", + "label": "Timeline start frame", + "default": 900000, + "minimum": 1, + "maximum": 10000000 + }, + { + "type": "number", + "key": "timeline_frame_offset", + "label": "Timeline frame offset", + "default": 0, + "minimum": -1000000, + "maximum": 1000000 + }, + { + "key": "subsets", + "label": "Subsets", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "family", + "label": "Family" + }, + { + "type": "list", + "key": "families", + "label": "Families", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "type": "list", + "key": "extensions", + "label": "Extensions", + "object_type": "text" + }, + { + "key": "version", + "label": "Version lock", + "type": "number", + "default": 0, + "minimum": 0, + "maximum": 10 + } + , + { + "type": "boolean", + "key": "keepSequence", + "label": "Keep sequence if used for review", + "default": false + } + ] + } + } + ] } ] } From 3deef5b0ded90dba2d3b268dacec4c2294811100 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:45:22 +0200 Subject: [PATCH 346/359] settings: adding `standalonepublisher` to hosts --- openpype/settings/entities/enum_entity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 63e0afeb47..d306eca7ef 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -139,7 +139,8 @@ class HostsEnumEntity(BaseEnumEntity): "photoshop", "resolve", "tvpaint", - "unreal" + "unreal", + "standalonepublisher" ] if self.use_empty_value: host_names.insert(0, "") From 5093360e549e01a19696d27ed407ccec920ba2d7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:58:29 +0200 Subject: [PATCH 347/359] standalone: prepare editorial plugin --- .../plugins/publish/collect_editorial.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py index fc9d95d3d7..5d61cb7f43 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py @@ -2,7 +2,7 @@ Optional: presets -> extensions ( example of use: - [".mov", ".mp4"] + ["mov", "mp4"] ) presets -> source_dir ( example of use: @@ -11,6 +11,7 @@ Optional: "{root[work]}/{project[name]}/inputs" "./input" "../input" + "" ) """ @@ -48,7 +49,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): actions = [] # presets - extensions = [".mov", ".mp4"] + extensions = ["mov", "mp4"] source_dir = None def process(self, instance): @@ -72,7 +73,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): video_path = None basename = os.path.splitext(os.path.basename(file_path))[0] - if self.source_dir: + if self.source_dir is not "": source_dir = self.source_dir.replace("\\", "/") if ("./" in source_dir) or ("../" in source_dir): # get current working dir @@ -98,7 +99,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): if os.path.splitext(f)[0] not in basename: continue # filter out by respected extensions - if os.path.splitext(f)[1] not in self.extensions: + if os.path.splitext(f)[1][1:] not in self.extensions: continue video_path = os.path.join( staging_dir, f From fa6148e3f0d0719f061c84a9ebf79cd73ebc83a0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 16:59:07 +0200 Subject: [PATCH 348/359] standalone: settings for editorial plugin --- .../project_settings/standalonepublisher.json | 7 +++++++ .../schema_project_standalonepublisher.json | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index 52020f2ce8..f08212934d 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -166,6 +166,13 @@ "output": [] } }, + "CollectEditorial": { + "source_dir": "", + "extensions": [ + "mov", + "mp4" + ] + }, "CollectHierarchyInstance": { "shot_rename_template": "{project[code]}_{_sequence_}_{_shot_}", "shot_rename_search_patterns": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json index 30144341c2..c627012531 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_standalonepublisher.json @@ -131,6 +131,26 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectEditorial", + "label": "Collect Editorial", + "is_group": true, + "children": [ + { + "type": "text", + "key": "source_dir", + "label": "Editorial resources pointer" + }, + { + "type": "list", + "key": "extensions", + "label": "Accepted extensions", + "object_type": "text" + } + ] + }, { "type": "dict", "collapsible": true, From a8858fa149493b7b052c3400fb1fd94a2d18b59a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 17:08:13 +0200 Subject: [PATCH 349/359] hound: suggestions --- .../standalonepublisher/plugins/publish/collect_editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py index 5d61cb7f43..0a1d29ccdc 100644 --- a/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py +++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_editorial.py @@ -73,7 +73,7 @@ class CollectEditorial(pyblish.api.InstancePlugin): video_path = None basename = os.path.splitext(os.path.basename(file_path))[0] - if self.source_dir is not "": + if self.source_dir != "": source_dir = self.source_dir.replace("\\", "/") if ("./" in source_dir) or ("../" in source_dir): # get current working dir From 1886f29a69dd07b84561c6b7478b503f711ef731 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 17:26:58 +0200 Subject: [PATCH 350/359] settings: global CleanUp --- .../defaults/project_settings/global.json | 4 ++++ .../schemas/schema_global_publish.json | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 037fa63a29..94d2b3bb3a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -184,6 +184,10 @@ ".*" ] } + }, + "CleanUp": { + "paterns": [], + "remove_temp_renders": false } }, "tools": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 496635287f..5c6cbe0e6f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -594,6 +594,30 @@ ] } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CleanUp", + "label": "Clean Up", + "is_group": true, + "children": [ + { + "type": "list", + "key": "paterns", + "label": "Paterrns (regex)", + "object_type": { + "type": "text" + } + }, + { + "type": "boolean", + "key": "remove_temp_renders", + "label": "Remove Temp renders", + "default": false + } + + ] } ] } From 66fbd2f4a36bc75e0a9de99f4789260ed3e31222 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 13 Jul 2021 17:48:54 +0200 Subject: [PATCH 351/359] settings: updating ProcessSubmittedJobOnFarm plugin --- .../defaults/project_settings/global.json | 2 ++ .../schemas/schema_global_publish.json | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 94d2b3bb3a..826b5ab465 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -172,6 +172,8 @@ "deadline_group": "", "deadline_chunk_size": 1, "deadline_priority": 50, + "publishing_script": "", + "skip_integration_repre_list": [], "aov_filter": { "maya": [ ".+(?:\\.|_)([Bb]eauty)(?:\\.|_).*" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5c6cbe0e6f..4715db4888 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -554,6 +554,22 @@ "key": "deadline_priority", "label": "Deadline Priotity" }, + { + "type": "splitter" + }, + { + "type": "text", + "key": "publishing_script", + "label": "Publishing script path" + }, + { + "type": "list", + "key": "skip_integration_repre_list", + "label": "Skip integration of representation with ext", + "object_type": { + "type": "text" + } + }, { "type": "dict", "key": "aov_filter", From abd02b945c71249d5ebd98b35bee2916a7b6a6fb Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 14 Jul 2021 03:42:08 +0000 Subject: [PATCH 352/359] [Automated] Bump version --- CHANGELOG.md | 17 +++++++++++++++-- openpype/version.py | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc659bd629..0ed0159a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ # Changelog +## [3.3.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) + +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) + +**🐛 Bug fixes** + +- Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) + +**Merged pull requests:** + +- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) + ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) -[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...3.2.0) +[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0) **🚀 Enhancements** @@ -25,6 +37,7 @@ **🐛 Bug fixes** - nuke: fixing wrong name of family folder when `used existing frames` [\#1803](https://github.com/pypeclub/OpenPype/pull/1803) +- Collect ftrack family bugs [\#1801](https://github.com/pypeclub/OpenPype/pull/1801) - Invitee email can be None which break the Ftrack commit. [\#1788](https://github.com/pypeclub/OpenPype/pull/1788) - Fix: staging and `--use-version` option [\#1786](https://github.com/pypeclub/OpenPype/pull/1786) - Otio unrelated error on import [\#1782](https://github.com/pypeclub/OpenPype/pull/1782) @@ -69,7 +82,7 @@ - Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727) -**Merged pull requests:** +**⚠️ Deprecations** - global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710) diff --git a/openpype/version.py b/openpype/version.py index 7bcd7face2..2fc2b4bc26 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.2.0" +__version__ = "3.3.0-nightly.1" From 7ab243cd29a400bdcde2e16269ad1b595ebe6db6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 15 Jul 2021 13:27:17 +0200 Subject: [PATCH 353/359] update acre in poetry lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 30dbe50c19..aad1898983 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,7 +11,7 @@ develop = false type = "git" url = "https://github.com/pypeclub/acre.git" reference = "master" -resolved_reference = "68784b7eb5b7bb5f409b61ab31d4403878a3e1b7" +resolved_reference = "5a812c6dcfd3aada87adb49be98c548c894d6566" [[package]] name = "aiohttp" From 1b6b3fe859ccf4c3f3f9deb5de7d852d8842ea2a Mon Sep 17 00:00:00 2001 From: jezscha Date: Thu, 15 Jul 2021 13:15:32 +0000 Subject: [PATCH 354/359] Create draft PR for #1828 From a62607ed7161388a3110bdf515762298d881623e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 15 Jul 2021 17:30:59 +0200 Subject: [PATCH 355/359] Nuke: settings create write with default subset names --- .../settings/defaults/project_settings/nuke.json | 15 +++++++++++++-- .../projects_schema/schema_project_nuke.json | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 71bf46d5b3..136f1d6b42 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -10,11 +10,22 @@ }, "create": { "CreateWriteRender": { - "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}" + "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}", + "defaults": [ + "Main", + "Mask" + ] }, "CreateWritePrerender": { "fpath_template": "{work}/prerenders/nuke/{subset}/{subset}.{frame}.{ext}", - "use_range_limit": true + "use_range_limit": true, + "defaults": [ + "Key01", + "Bg01", + "Fg01", + "Branch01", + "Part01" + ] } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 01a954f283..e0b21f4037 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -63,6 +63,14 @@ "type": "text", "key": "fpath_template", "label": "Path template" + }, + { + "type": "list", + "key": "defaults", + "label": "Subset name defaults", + "object_type": { + "type": "text" + } } ] }, @@ -82,6 +90,14 @@ "type": "boolean", "key": "use_range_limit", "label": "Use Frame range limit by default" + }, + { + "type": "list", + "key": "defaults", + "label": "Subset name defaults", + "object_type": { + "type": "text" + } } ] } From 924300324666c3d620ab25250846af03042d5869 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 17 Jul 2021 03:41:20 +0000 Subject: [PATCH 356/359] [Automated] Bump version --- CHANGELOG.md | 17 +++++++++-------- openpype/version.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ed0159a4d..467ed7c0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,21 @@ # Changelog -## [3.3.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) +**🚀 Enhancements** + +- nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829) +- Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) +- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) + **🐛 Bug fixes** +- Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) +- Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) -**Merged pull requests:** - -- Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) - ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0) @@ -55,8 +59,6 @@ - StandalonePublisher: failing collector for editorial [\#1738](https://github.com/pypeclub/OpenPype/pull/1738) - Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737) - TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735) -- Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679) -- Unreal: launching on Linux [\#1672](https://github.com/pypeclub/OpenPype/pull/1672) **Merged pull requests:** @@ -117,7 +119,6 @@ - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) - Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) -- Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 2fc2b4bc26..00df9438eb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.1" +__version__ = "3.3.0-nightly.2" From 38cf278b7fd9db97e8a04b9a0340d16fd5f511c0 Mon Sep 17 00:00:00 2001 From: jezscha Date: Mon, 19 Jul 2021 13:22:58 +0000 Subject: [PATCH 357/359] Create draft PR for #1835 From aee7ed3da1f5811a3f5387c05f7241f297cd310c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 20 Jul 2021 08:54:11 +0200 Subject: [PATCH 358/359] nuke: fix write node name not Crop01 --- openpype/hosts/nuke/api/lib.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index d7f3fdc6ba..ee03e04360 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -391,13 +391,14 @@ def create_write_node(name, data, input=None, prenodes=None, if prenodes: for node in prenodes: # get attributes - name = node["name"] + pre_node_name = node["name"] klass = node["class"] knobs = node["knobs"] dependent = node["dependent"] # create node - now_node = nuke.createNode(klass, "name {}".format(name)) + now_node = nuke.createNode( + klass, "name {}".format(pre_node_name)) now_node.hideControlPanel() # add data to knob @@ -476,27 +477,27 @@ def create_write_node(name, data, input=None, prenodes=None, linked_knob_names.append("Render") - for name in linked_knob_names: - if "_grp-start_" in name: + for _k_name in linked_knob_names: + if "_grp-start_" in _k_name: knob = nuke.Tab_Knob( "rnd_attr", "Rendering attributes", nuke.TABBEGINCLOSEDGROUP) GN.addKnob(knob) - elif "_grp-end_" in name: + elif "_grp-end_" in _k_name: knob = nuke.Tab_Knob( "rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP) GN.addKnob(knob) else: - if "___" in name: + if "___" in _k_name: # add devider GN.addKnob(nuke.Text_Knob("")) else: - # add linked knob by name + # add linked knob by _k_name link = nuke.Link_Knob("") - link.makeLink(write_node.name(), name) - link.setName(name) + link.makeLink(write_node.name(), _k_name) + link.setName(_k_name) # make render - if "Render" in name: + if "Render" in _k_name: link.setLabel("Render Local") link.setFlag(0x1000) GN.addKnob(link) From 33d26d85bdcd1961c9dadad5c42299209224c025 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 21 Jul 2021 03:42:15 +0000 Subject: [PATCH 359/359] [Automated] Bump version --- CHANGELOG.md | 14 ++++++++------ openpype/version.py | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 467ed7c0a4..0ecd583191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,34 @@ # Changelog -## [3.3.0-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.3.0-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.2.0...HEAD) **🚀 Enhancements** - nuke: settings create missing default subsets [\#1829](https://github.com/pypeclub/OpenPype/pull/1829) +- Update poetry lock [\#1823](https://github.com/pypeclub/OpenPype/pull/1823) - Settings: settings for plugins [\#1819](https://github.com/pypeclub/OpenPype/pull/1819) +- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) - Maya: Deadline custom settings [\#1797](https://github.com/pypeclub/OpenPype/pull/1797) **🐛 Bug fixes** +- nuke: write render node skipped with crop [\#1836](https://github.com/pypeclub/OpenPype/pull/1836) - Project folder structure overrides [\#1813](https://github.com/pypeclub/OpenPype/pull/1813) - Maya: fix yeti settings path in extractor [\#1809](https://github.com/pypeclub/OpenPype/pull/1809) - Houdini colector formatting keys fix [\#1802](https://github.com/pypeclub/OpenPype/pull/1802) +**Merged pull requests:** + +- PS, AE - send actual context when another webserver is running [\#1811](https://github.com/pypeclub/OpenPype/pull/1811) + ## [3.2.0](https://github.com/pypeclub/OpenPype/tree/3.2.0) (2021-07-13) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.7...3.2.0) **🚀 Enhancements** -- Nuke: ftrack family plugin settings preset [\#1805](https://github.com/pypeclub/OpenPype/pull/1805) - Standalone publisher last project [\#1799](https://github.com/pypeclub/OpenPype/pull/1799) - Ftrack Multiple notes as server action [\#1795](https://github.com/pypeclub/OpenPype/pull/1795) - Settings conditional dict [\#1777](https://github.com/pypeclub/OpenPype/pull/1777) @@ -110,20 +116,16 @@ - Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702) - OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694) - Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689) -- \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683) -- Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682) **🐛 Bug fixes** - Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707) - Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705) - Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691) -- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687) **Merged pull requests:** - update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697) -- Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686) # Changelog diff --git a/openpype/version.py b/openpype/version.py index 00df9438eb..bbf93baec0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.3.0-nightly.2" +__version__ = "3.3.0-nightly.3"