From bb6d2ac71a7fbdd8290e22ed026fc376811c3ccd Mon Sep 17 00:00:00 2001 From: Ben Souchet Date: Wed, 29 Jan 2025 10:59:18 +0100 Subject: [PATCH 1/9] Bump version to 4.0.24 --- src/quadpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/quadpype/version.py b/src/quadpype/version.py index 0644a5aff7..5870ba2c1c 100644 --- a/src/quadpype/version.py +++ b/src/quadpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """File declaring QuadPype version.""" -__version__ = "4.0.23" +__version__ = "4.0.24" From ffc5d0ab2d4f0636643395cbf047c1de44d6aba5 Mon Sep 17 00:00:00 2001 From: hfarre Date: Thu, 30 Jan 2025 10:21:30 +0100 Subject: [PATCH 2/9] Add a validator and repair to make sure all objects are publish by instances. --- src/quadpype/hosts/blender/api/__init__.py | 13 ++ .../hosts/blender/api/template_resolving.py | 172 ++++++++++++++++++ .../publish/validate_model_contents.py | 91 +++++++++ .../defaults/project_settings/blender.json | 12 ++ .../schema_project_blender.json | 161 ++++++++++++++++ 5 files changed, 449 insertions(+) create mode 100644 src/quadpype/hosts/blender/api/template_resolving.py create mode 100644 src/quadpype/hosts/blender/plugins/publish/validate_model_contents.py diff --git a/src/quadpype/hosts/blender/api/__init__.py b/src/quadpype/hosts/blender/api/__init__.py index 70824fe326..e7b709687f 100644 --- a/src/quadpype/hosts/blender/api/__init__.py +++ b/src/quadpype/hosts/blender/api/__init__.py @@ -41,6 +41,13 @@ from .render_lib import prepare_rendering +from .template_resolving import ( + get_resolved_name, + get_entity_collection_template, + get_task_collection_template, + update_parent_data_with_entity_prefix +) + __all__ = [ "install", @@ -71,4 +78,10 @@ "capture", # "unique_name", "prepare_rendering", + + #Templates for working: + "get_resolved_name", + "get_entity_collection_template", + "get_task_collection_template", + "update_parent_data_with_entity_prefix" ] diff --git a/src/quadpype/hosts/blender/api/template_resolving.py b/src/quadpype/hosts/blender/api/template_resolving.py new file mode 100644 index 0000000000..247c2694b9 --- /dev/null +++ b/src/quadpype/hosts/blender/api/template_resolving.py @@ -0,0 +1,172 @@ +from collections import OrderedDict + +from quadpype.settings import get_project_settings +from quadpype.lib import ( + filter_profiles, + Logger, + StringTemplate, +) + +def get_resolved_name(data, template): + """Resolve template_collections_naming with entered data. + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + template (list): template to solve + Returns: + str: Resolved template + """ + template_obj = StringTemplate(template) + # Resolve the template + output = template_obj.format_strict(data) + if output: + return output.normalized() + return output + +def _get_project_name_by_data(data): + """ + Retrieve the project settings depending on given data + This can be given by an instance or an app, and they are not sorted the same way + + Return: + str, bool: The project name (str) and a bool to specify if this is from anatomy or project (bool) + """ + is_from_anatomy = False + if data.get("project"): + return data["project"]["name"], is_from_anatomy + if data.get("anatomyData"): + is_from_anatomy = True + return data["anatomyData"]["project"]["name"], is_from_anatomy + +def _get_app_name_by_data(data): + """ + Retrieve the project settings depending on given data + This can be given by an instance or an app, and they are not sorted the same way + + Return: + str, bool: The app name (str) and a bool to specify if this is from anatomy or project (bool) + """ + is_from_anatomy = False + if data.get("app"): + return data["project"]["app"], is_from_anatomy + if data.get("anatomyData"): + is_from_anatomy = True + return data["anatomyData"]["app"], is_from_anatomy + +def _get_parent_by_data(data): + """ + Retrieve the project settings depending on given data + This can be given by an instance or an app, and they are not sorted the same way + + Return: + str, bool: The parent name (str) and a bool to specify if this is from anatomy or project (bool) + """ + is_from_anatomy = False + if data.get("parent"): + return data["parent"], is_from_anatomy + if data.get("anatomyData"): + is_from_anatomy = True + return data["anatomyData"]["parent"], is_from_anatomy + +def _get_profiles(setting_key, data, project_settings=None): + + if not project_settings: + project_settings = get_project_settings(_get_project_name_by_data(data)) + + # Get Entity Type Name Matcher Profiles + try: + profiles = ( + project_settings + [_get_app_name_by_data(data)] + ["templated_workfile_build"] + [setting_key] + ["profiles"] + ) + + except Exception: + raise KeyError("Project has no profiles set for {}".format(setting_key)) + + # By default, profiles = [], so we must stop if there's no profiles set + if not profiles: + raise KeyError("Project has no profiles set for {}".format(setting_key)) + + return profiles + +def _get_entity_prefix(data): + """Retrieve the asset_type (entity_type) short name for proper blender naming + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + Return: + str: A string corresponding to the short name for entered entity type + bool: bool to specify if this is from anatomy or project (bool) + """ + + # Get Entity Type Name Matcher Profiles + profiles = _get_profiles("entity_type_name_matcher", data) + parent, is_anatomy = _get_parent_by_data(data) + + profile_key = {"entity_types": parent} + profile = filter_profiles(profiles, profile_key) + # If a profile is found, return the prefix + if profile.get("entity_prefix"): + return profile["entity_prefix"], is_anatomy + + return None + +def update_parent_data_with_entity_prefix(data): + """ + Will update the input data dict to change the value of the ["parent"] key + to become the corresponding prefix + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + Return: + dict: Data updated with new ["parent"] prefix + """ + parent_prefix, is_anatomy = _get_entity_prefix(data) + if parent_prefix and not is_anatomy: + data["parent"] = parent_prefix + return data + + if parent_prefix and is_anatomy: + data["anatomyData"]["parent"] = parent_prefix + return data + +def get_entity_collection_template(data): + """Retrieve the template for the collection depending on the entity type + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + Return: + str: A template that can be solved later + """ + + # Get Entity Type Name Matcher Profiles + profiles = _get_profiles("collections_templates_by_entity_type", data) + parent, is_anatomy = _get_parent_by_data(data) + profile_key = {"entity_types": parent} + profile = filter_profiles(profiles, profile_key) + # If a profile is found, return the template + if profile.get("template"): + return profile["template"] + + return None + +def get_task_collection_template(data): + """Retrieve the template for the collection depending on the entity type + Args: + data (Dict[str, Any]): Data to fill template_collections_naming. + Return: + str: A template that can be solved later + """ + + # Get Entity Type Name Matcher Profiles + profiles = _get_profiles("working_collections_templates_by_tasks", data) + profile_key = {"task_types": data["task"]} + profile = filter_profiles(profiles, profile_key) + + # If a profile is found, return the template + if profile and data.get("variant", None) == "Main": + return profile["main_template"] + + if profile and data.get("variant", None) != "Main": + return profile["variant_template"] + + return None diff --git a/src/quadpype/hosts/blender/plugins/publish/validate_model_contents.py b/src/quadpype/hosts/blender/plugins/publish/validate_model_contents.py new file mode 100644 index 0000000000..89c06fbf27 --- /dev/null +++ b/src/quadpype/hosts/blender/plugins/publish/validate_model_contents.py @@ -0,0 +1,91 @@ +import inspect +import bpy + +from quadpype.hosts.blender.api import plugin, action + +from quadpype.pipeline.publish import ( + ValidateContentsOrder, + PublishValidationError, + RepairAction +) + +from quadpype.hosts.blender.api import ( + get_resolved_name, + get_task_collection_template +) + +class ValidateModelContents(plugin.BlenderInstancePlugin): + """Validates Model instance contents. + + A Model instance should have everything that is in the {parent}-{asset} collection + """ + + order = ValidateContentsOrder + families = ['model'] + hosts = ['blender'] + label = 'Validate Model Contents' + + actions = [action.SelectInvalidAction, RepairAction] + + @staticmethod + def get_invalid(instance): + # Get collection template from task/variant + template = get_task_collection_template(instance.data) + coll_name = get_resolved_name(instance.data, template) + + # Get collection + asset_model_coll = bpy.data.collections.get(coll_name) + if not asset_model_coll: + raise RuntimeError("No collection found with name :" + "{}".format(asset_model_coll)) + + objects = [obj for obj in instance] + asset_model_list = asset_model_coll.objects + + # Get objects in instance + objects = [obj for obj in instance] + + # Compare obj in instance and obj in scene model collection + invalid = [missing_obj for missing_obj in asset_model_list if missing_obj not in objects] + + return invalid + + def process(self, instance): + + invalid = self.get_invalid(instance) + + if invalid: + names = ", ".join(obj.name for obj in invalid) + raise PublishValidationError( + "Objects found in collection which are not" + f" in instance: {names}", + description=self.get_description() + ) + + @classmethod + def repair(cls, instance): + + invalid = cls.get_invalid(instance) + if not invalid: + return + + instance_object = instance.data.get("transientData").get("instance_node") + if not instance_object: + raise RuntimeError ("No instance object found for {}".format(instance.name)) + + for object in invalid: + object.parent = instance_object + + def get_description(self): + return inspect.cleandoc( + """## Some objects are missing in the publish instance. + + Based on the variant name, it appears that all the objects + in the model collection are not present in the + publish instance. + + You can either: + - Select them with the Select Invalid button + - Auto Repair and put them in the corresponding publish instance + """ + ) diff --git a/src/quadpype/settings/defaults/project_settings/blender.json b/src/quadpype/settings/defaults/project_settings/blender.json index d7f1abdb06..b4c8f02a51 100644 --- a/src/quadpype/settings/defaults/project_settings/blender.json +++ b/src/quadpype/settings/defaults/project_settings/blender.json @@ -35,6 +35,18 @@ "create_first_version": false, "custom_templates": [] }, + "templated_workfile_build": { + "profiles": [], + "entity_type_name_matcher":{ + "profiles": [] + }, + "collections_templates_by_entity_type": { + "profiles": [] + }, + "working_collections_templates_by_tasks": { + "profiles": [] + } + }, "publish": { "ValidateCameraZeroKeyframe": { "enabled": true, diff --git a/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json b/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json index 58a271edb6..f6abbcb465 100644 --- a/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json +++ b/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json @@ -234,6 +234,167 @@ "workfile_builder/builder_on_start", "workfile_builder/profiles" ] + }, + { + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "path", + "label": "Path to template", + "type": "path", + "multiplatform": false, + "multipath": false + }, + { + "key": "keep_placeholder", + "label": "Keep placeholders", + "type": "boolean", + "default": true + }, + { + "key": "create_first_version", + "label": "Create first version", + "type": "boolean", + "default": true + }, + { + "key": "autobuild_first_version", + "label": "Autobuild first version", + "type": "boolean", + "default": true + } + ] + } + }, + { + "type": "dict", + "collapsible": true, + "key": "entity_type_name_matcher", + "label": "Entity Type Name Matcher", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "type": "enum", + "key":"entity_types", + "label":"Entity Types", + "multiselection": true, + "enum_items": [ + {"3D_Character": "3D_Character"}, + {"3D_Prop": "3D_Prop"}, + {"3D_Environment": "3D_Environment"}, + {"CHARA": "CHARA"}, + {"PROPS": "PROPS"}, + {"3D_ENV": "3D_ENV"} + ] + }, + { + "type": "text", + "key": "entity_prefix", + "label": "Entity Prefix" + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "collections_templates_by_entity_type", + "label": "Collections Templates By Entity Type", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "type": "enum", + "key":"entity_types", + "label":"Entity Types", + "multiselection": true, + "enum_items": [ + {"3D_Character": "3D_Character"}, + {"3D_Prop": "3D_Prop"}, + {"3D_Environment": "3D_Environment"}, + {"CHARA": "CHARA"}, + {"PROPS": "PROPS"}, + {"3D_ENV": "3D_ENV"} + ] + }, + { + "type": "text", + "key": "template", + "label": "Template" + } + ] + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "working_collections_templates_by_tasks", + "label": "Working Collections Templates By Tasks", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "type": "text", + "key": "main_template", + "label": "Main Template" + }, + { + "type": "text", + "key": "variant_template", + "label": "Variant Template" + } + ] + } + } + ] + } + ] }, { "type": "schema", From 678b0b01ef7dea2847cb000b83137f12b755dd1d Mon Sep 17 00:00:00 2001 From: hfarre Date: Thu, 30 Jan 2025 15:10:41 +0100 Subject: [PATCH 3/9] Rework code, and change settings enum type to text type --- .../hosts/blender/api/template_resolving.py | 81 ++++++++++--------- .../schema_project_blender.json | 26 +----- 2 files changed, 49 insertions(+), 58 deletions(-) diff --git a/src/quadpype/hosts/blender/api/template_resolving.py b/src/quadpype/hosts/blender/api/template_resolving.py index 247c2694b9..b0f0808d32 100644 --- a/src/quadpype/hosts/blender/api/template_resolving.py +++ b/src/quadpype/hosts/blender/api/template_resolving.py @@ -18,65 +18,78 @@ def get_resolved_name(data, template): template_obj = StringTemplate(template) # Resolve the template output = template_obj.format_strict(data) - if output: - return output.normalized() - return output + return output.normalized() def _get_project_name_by_data(data): """ - Retrieve the project settings depending on given data + Retrieve the project name depending on given data This can be given by an instance or an app, and they are not sorted the same way Return: str, bool: The project name (str) and a bool to specify if this is from anatomy or project (bool) """ + project_name = None is_from_anatomy = False + if data.get("project"): - return data["project"]["name"], is_from_anatomy - if data.get("anatomyData"): + project_name = data["project"]["name"] + elif data.get("anatomyData"): is_from_anatomy = True - return data["anatomyData"]["project"]["name"], is_from_anatomy + project_name = data["anatomyData"]["project"]["name"] + + return project_name, is_from_anatomy def _get_app_name_by_data(data): """ - Retrieve the project settings depending on given data + Retrieve the app name depending on given data This can be given by an instance or an app, and they are not sorted the same way Return: str, bool: The app name (str) and a bool to specify if this is from anatomy or project (bool) """ + app_name = None is_from_anatomy = False + if data.get("app"): - return data["project"]["app"], is_from_anatomy - if data.get("anatomyData"): + app_name = data["project"]["app"] + elif data.get("anatomyData"): is_from_anatomy = True - return data["anatomyData"]["app"], is_from_anatomy + app_name= data["anatomyData"]["app"] + + return app_name, is_from_anatomy def _get_parent_by_data(data): """ - Retrieve the project settings depending on given data + Retrieve the parent asset name depending on given data This can be given by an instance or an app, and they are not sorted the same way Return: str, bool: The parent name (str) and a bool to specify if this is from anatomy or project (bool) """ + parent_name = None is_from_anatomy = False + if data.get("parent"): - return data["parent"], is_from_anatomy - if data.get("anatomyData"): + parent_name = data["parent"] + elif data.get("anatomyData"): is_from_anatomy = True - return data["anatomyData"]["parent"], is_from_anatomy + parent_name = data["anatomyData"]["parent"] + + return parent_name, is_from_anatomy def _get_profiles(setting_key, data, project_settings=None): + project_name, is_anatomy_data = _get_project_name_by_data(data) + app_name, is_anatomy_data = _get_app_name_by_data(data) + if not project_settings: - project_settings = get_project_settings(_get_project_name_by_data(data)) + project_settings = get_project_settings(project_name) # Get Entity Type Name Matcher Profiles try: profiles = ( project_settings - [_get_app_name_by_data(data)] + [app_name] ["templated_workfile_build"] [setting_key] ["profiles"] @@ -107,10 +120,7 @@ def _get_entity_prefix(data): profile_key = {"entity_types": parent} profile = filter_profiles(profiles, profile_key) # If a profile is found, return the prefix - if profile.get("entity_prefix"): - return profile["entity_prefix"], is_anatomy - - return None + return profile.get("entity_prefix"), is_anatomy def update_parent_data_with_entity_prefix(data): """ @@ -118,17 +128,16 @@ def update_parent_data_with_entity_prefix(data): to become the corresponding prefix Args: data (Dict[str, Any]): Data to fill template_collections_naming. - Return: - dict: Data updated with new ["parent"] prefix """ parent_prefix, is_anatomy = _get_entity_prefix(data) - if parent_prefix and not is_anatomy: - data["parent"] = parent_prefix - return data - if parent_prefix and is_anatomy: + if not parent_prefix: + return None + + if is_anatomy: data["anatomyData"]["parent"] = parent_prefix - return data + else: + data["parent"] = parent_prefix def get_entity_collection_template(data): """Retrieve the template for the collection depending on the entity type @@ -144,13 +153,11 @@ def get_entity_collection_template(data): profile_key = {"entity_types": parent} profile = filter_profiles(profiles, profile_key) # If a profile is found, return the template - if profile.get("template"): - return profile["template"] + return profile.get("template") - return None def get_task_collection_template(data): - """Retrieve the template for the collection depending on the entity type + """Retrieve the template for the collection depending on the task type Args: data (Dict[str, Any]): Data to fill template_collections_naming. Return: @@ -162,11 +169,13 @@ def get_task_collection_template(data): profile_key = {"task_types": data["task"]} profile = filter_profiles(profiles, profile_key) + if not profile: + return None # If a profile is found, return the template - if profile and data.get("variant", None) == "Main": - return profile["main_template"] + if data.get("variant", None) == "Main": + return profile["main_template"] - if profile and data.get("variant", None) != "Main": - return profile["variant_template"] + elif data.get("variant", None) != "Main": + return profile["variant_template"] return None diff --git a/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json b/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json index f6abbcb465..dd9660e57c 100644 --- a/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json +++ b/src/quadpype/settings/entities/schemas/project_schema/schema_project_blender.json @@ -301,18 +301,9 @@ "type": "dict", "children": [ { - "type": "enum", + "type": "text", "key":"entity_types", - "label":"Entity Types", - "multiselection": true, - "enum_items": [ - {"3D_Character": "3D_Character"}, - {"3D_Prop": "3D_Prop"}, - {"3D_Environment": "3D_Environment"}, - {"CHARA": "CHARA"}, - {"PROPS": "PROPS"}, - {"3D_ENV": "3D_ENV"} - ] + "label":"Entity Types" }, { "type": "text", @@ -338,18 +329,9 @@ "type": "dict", "children": [ { - "type": "enum", + "type": "text", "key":"entity_types", - "label":"Entity Types", - "multiselection": true, - "enum_items": [ - {"3D_Character": "3D_Character"}, - {"3D_Prop": "3D_Prop"}, - {"3D_Environment": "3D_Environment"}, - {"CHARA": "CHARA"}, - {"PROPS": "PROPS"}, - {"3D_ENV": "3D_ENV"} - ] + "label":"Entity Types" }, { "type": "text", From dd635f3884884d5fc14befc2a5894eb41f056719 Mon Sep 17 00:00:00 2001 From: hfarre Date: Thu, 30 Jan 2025 16:41:03 +0100 Subject: [PATCH 4/9] Fine tuning code and typo in space --- src/quadpype/hosts/blender/api/template_resolving.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/quadpype/hosts/blender/api/template_resolving.py b/src/quadpype/hosts/blender/api/template_resolving.py index b0f0808d32..6fcd84568e 100644 --- a/src/quadpype/hosts/blender/api/template_resolving.py +++ b/src/quadpype/hosts/blender/api/template_resolving.py @@ -54,7 +54,7 @@ def _get_app_name_by_data(data): app_name = data["project"]["app"] elif data.get("anatomyData"): is_from_anatomy = True - app_name= data["anatomyData"]["app"] + app_name = data["anatomyData"]["app"] return app_name, is_from_anatomy @@ -132,7 +132,7 @@ def update_parent_data_with_entity_prefix(data): parent_prefix, is_anatomy = _get_entity_prefix(data) if not parent_prefix: - return None + return if is_anatomy: data["anatomyData"]["parent"] = parent_prefix @@ -175,7 +175,4 @@ def get_task_collection_template(data): if data.get("variant", None) == "Main": return profile["main_template"] - elif data.get("variant", None) != "Main": - return profile["variant_template"] - - return None + return profile["variant_template"] From 31d70e62894fab0cb40e19e75f3c88d601aae8bf Mon Sep 17 00:00:00 2001 From: Ben Souchet Date: Fri, 31 Jan 2025 14:02:36 +0100 Subject: [PATCH 5/9] Fix Sync server root retrieval + add custom pycharm config --- .gitignore | 2 +- .run/{custom_pre.run.xml => custom.run.xml} | 17 ++++++++--------- src/quadpype/pipeline/anatomy.py | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) rename .run/{custom_pre.run.xml => custom.run.xml} (59%) diff --git a/.gitignore b/.gitignore index 971015c59e..eceadd6efb 100644 --- a/.gitignore +++ b/.gitignore @@ -112,7 +112,7 @@ install-pyenv-win.ps1 # Developer tools src/tools/dev_* .github_changelog_generator - +.run/custom_pre.run.xml # Addons ######## diff --git a/.run/custom_pre.run.xml b/.run/custom.run.xml similarity index 59% rename from .run/custom_pre.run.xml rename to .run/custom.run.xml index 1d5cbd9f04..32baacd8c0 100644 --- a/.run/custom_pre.run.xml +++ b/.run/custom.run.xml @@ -1,29 +1,28 @@ - + + diff --git a/src/quadpype/pipeline/anatomy.py b/src/quadpype/pipeline/anatomy.py index 654d785fdb..18f5ff6513 100644 --- a/src/quadpype/pipeline/anatomy.py +++ b/src/quadpype/pipeline/anatomy.py @@ -447,7 +447,7 @@ def get_sync_server_addon(cls): if cls._sync_server_addon_cache.is_outdated: manager = ModulesManager() cls._sync_server_addon_cache.update_data( - manager.get_enabled_module("sitesync") + manager.get_enabled_module("sync_server") ) return cls._sync_server_addon_cache.data From 16722c8b032ce9d3497a8884bb878b26d7be8d28 Mon Sep 17 00:00:00 2001 From: Ben Souchet Date: Fri, 31 Jan 2025 14:05:24 +0100 Subject: [PATCH 6/9] Add pycharm pre custom config --- .run/custom_pre.run.xml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .run/custom_pre.run.xml diff --git a/.run/custom_pre.run.xml b/.run/custom_pre.run.xml new file mode 100644 index 0000000000..1d5cbd9f04 --- /dev/null +++ b/.run/custom_pre.run.xml @@ -0,0 +1,29 @@ + + + + + + + From 8a67f12e53e034fd955d1d0d0399b8531fbf8c21 Mon Sep 17 00:00:00 2001 From: Ben Souchet Date: Fri, 31 Jan 2025 14:06:27 +0100 Subject: [PATCH 7/9] Add pycharm pre custom config --- .run/custom.run.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.run/custom.run.xml b/.run/custom.run.xml index 32baacd8c0..ae980785b2 100644 --- a/.run/custom.run.xml +++ b/.run/custom.run.xml @@ -8,6 +8,9 @@