From e25fcade98ac6c787f99e50214b6b52099abd80f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 2 Dec 2021 19:02:18 +0100 Subject: [PATCH 001/105] move version detection --- igniter/bootstrap_repos.py | 210 +++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 151597e505..b0f3b482ac 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -232,6 +232,216 @@ class OpenPypeVersion(semver.VersionInfo): else: return hash(str(self)) + @staticmethod + def is_version_in_dir( + dir_item: Path, version: OpenPypeVersion) -> Tuple[bool, str]: + """Test if path item is OpenPype version matching detected version. + + If item is directory that might (based on it's name) + contain OpenPype version, check if it really does contain + OpenPype and that their versions matches. + + Args: + dir_item (Path): Directory to test. + version (OpenPypeVersion): OpenPype version detected + from name. + + Returns: + Tuple: State and reason, True if it is valid OpenPype version, + False otherwise. + + """ + try: + # add one 'openpype' level as inside dir there should + # be many other repositories. + version_str = OpenPypeVersion.get_version_string_from_directory( + dir_item) # noqa: E501 + version_check = OpenPypeVersion(version=version_str) + except ValueError: + return False, f"cannot determine version from {dir_item}" + + version_main = version_check.get_main_version() + detected_main = version.get_main_version() + if version_main != detected_main: + return False, (f"dir version ({version}) and " + f"its content version ({version_check}) " + "doesn't match. Skipping.") + return True, "Versions match" + + @staticmethod + def is_version_in_zip( + zip_item: Path, version: OpenPypeVersion) -> Tuple[bool, str]: + """Test if zip path is OpenPype version matching detected version. + + Open zip file, look inside and parse version from OpenPype + inside it. If there is none, or it is different from + version specified in file name, skip it. + + Args: + zip_item (Path): Zip file to test. + version (OpenPypeVersion): Pype version detected + from name. + + Returns: + Tuple: State and reason, True if it is valid OpenPype version, + False otherwise. + + """ + # skip non-zip files + if zip_item.suffix.lower() != ".zip": + return False, "Not a zip" + + try: + with ZipFile(zip_item, "r") as zip_file: + with zip_file.open( + "openpype/version.py") as version_file: + zip_version = {} + exec(version_file.read(), zip_version) + try: + version_check = OpenPypeVersion( + version=zip_version["__version__"]) + except ValueError as e: + return False, str(e) + + version_main = version_check.get_main_version() # + # noqa: E501 + detected_main = version.get_main_version() + # noqa: E501 + + if version_main != detected_main: + return False, (f"zip version ({version}) " + f"and its content version " + f"({version_check}) " + "doesn't match. Skipping.") + except BadZipFile: + return False, f"{zip_item} is not a zip file" + except KeyError: + return False, "Zip does not contain OpenPype" + return True, "Versions match" + + @staticmethod + def get_version_string_from_directory(repo_dir: Path) -> Union[str, None]: + """Get version of OpenPype in given directory. + + Note: in frozen OpenPype installed in user data dir, this must point + one level deeper as it is: + `openpype-version-v3.0.0/openpype/version.py` + + Args: + repo_dir (Path): Path to OpenPype repo. + + Returns: + str: version string. + None: if OpenPype is not found. + + """ + # try to find version + version_file = Path(repo_dir) / "openpype" / "version.py" + if not version_file.exists(): + return None + + version = {} + with version_file.open("r") as fp: + exec(fp.read(), version) + + return version['__version__'] + + @staticmethod + def get_available_versions( + staging: bool = False, local: bool = False) -> List: + """Get ordered dict of detected OpenPype version. + + Resolution order for OpenPype is following: + + 1) First we test for ``OPENPYPE_PATH`` environment variable + 2) We try to find ``openPypePath`` in registry setting + 3) We use user data directory + + Only versions from 3) will be listed when ``local`` is set to True. + + Args: + staging (bool, optional): List staging versions if True. + local (bool, optional): List only local versions. + + """ + registry = OpenPypeSettingsRegistry() + dir_to_search = Path(user_data_dir("openpype", "pypeclub")) + user_versions = OpenPypeVersion.get_versions_from_directory( + dir_to_search) + # if we have openpype_path specified, search only there. + + if not local: + if os.getenv("OPENPYPE_PATH"): + if Path(os.getenv("OPENPYPE_PATH")).exists(): + dir_to_search = Path(os.getenv("OPENPYPE_PATH")) + else: + try: + registry_dir = Path( + str(registry.get_item("openPypePath"))) + if registry_dir.exists(): + dir_to_search = registry_dir + + except ValueError: + # nothing found in registry, we'll use data dir + pass + + openpype_versions = OpenPypeVersion.get_versions_from_directory( + dir_to_search) + openpype_versions += user_versions + + # remove duplicates and staging/production + openpype_versions = [ + v for v in openpype_versions if v.is_staging() == staging + ] + openpype_versions = sorted(list(set(openpype_versions))) + + return openpype_versions + + @staticmethod + def get_versions_from_directory(openpype_dir: Path) -> List: + """Get all detected OpenPype versions in directory. + + Args: + openpype_dir (Path): Directory to scan. + + Returns: + list of OpenPypeVersion + + Throws: + ValueError: if invalid path is specified. + + """ + if not openpype_dir.exists() and not openpype_dir.is_dir(): + raise ValueError("specified directory is invalid") + + _openpype_versions = [] + # iterate over directory in first level and find all that might + # contain OpenPype. + for item in openpype_dir.iterdir(): + + # if file, strip extension, in case of dir not. + name = item.name if item.is_dir() else item.stem + result = OpenPypeVersion.version_in_str(name) + + if result: + detected_version: OpenPypeVersion + detected_version = result + + if item.is_dir() and not OpenPypeVersion.is_version_in_dir( + item, detected_version + )[0]: + continue + + if item.is_file() and not OpenPypeVersion.is_version_in_zip( + item, detected_version + )[0]: + continue + + detected_version.path = item + _openpype_versions.append(detected_version) + + return sorted(_openpype_versions) + class BootstrapRepos: """Class for bootstrapping local OpenPype installation. From 383307d88803404c1bac74366797b5a23e2fc5e0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 3 Dec 2021 13:51:03 +0100 Subject: [PATCH 002/105] tweak tests and add get latest version --- igniter/bootstrap_repos.py | 97 +++++++++------------- start.py | 1 - tests/unit/igniter/test_bootstrap_repos.py | 7 +- 3 files changed, 41 insertions(+), 64 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index b0f3b482ac..ca64d193c7 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -401,16 +401,16 @@ class OpenPypeVersion(semver.VersionInfo): def get_versions_from_directory(openpype_dir: Path) -> List: """Get all detected OpenPype versions in directory. - Args: - openpype_dir (Path): Directory to scan. + Args: + openpype_dir (Path): Directory to scan. - Returns: - list of OpenPypeVersion + Returns: + list of OpenPypeVersion - Throws: - ValueError: if invalid path is specified. + Throws: + ValueError: if invalid path is specified. - """ + """ if not openpype_dir.exists() and not openpype_dir.is_dir(): raise ValueError("specified directory is invalid") @@ -442,6 +442,26 @@ class OpenPypeVersion(semver.VersionInfo): return sorted(_openpype_versions) + @staticmethod + def get_latest_version( + staging: bool = False, local: bool = False) -> OpenPypeVersion: + """Get latest available version. + + This is utility version to get latest version from all found. + + Args: + staging (bool, optional): List staging versions if True. + local (bool, optional): List only local versions. + + See also: + OpenPypeVersion.get_available_versions() + + """ + openpype_versions = OpenPypeVersion.get_available_versions( + staging, local) + + return openpype_versions[-1] + class BootstrapRepos: """Class for bootstrapping local OpenPype installation. @@ -944,66 +964,23 @@ class BootstrapRepos: os.environ["PYTHONPATH"] = os.pathsep.join(paths) + @staticmethod def find_openpype( - self, openpype_path: Union[Path, str] = None, staging: bool = False, include_zips: bool = False) -> Union[List[OpenPypeVersion], None]: - """Get ordered dict of detected OpenPype version. - Resolution order for OpenPype is following: - - 1) First we test for ``OPENPYPE_PATH`` environment variable - 2) We try to find ``openPypePath`` in registry setting - 3) We use user data directory - - Args: - openpype_path (Path or str, optional): Try to find OpenPype on - the given path or url. - staging (bool, optional): Filter only staging version, skip them - otherwise. - include_zips (bool, optional): If set True it will try to find - OpenPype in zip files in given directory. - - Returns: - dict of Path: Dictionary of detected OpenPype version. - Key is version, value is path to zip file. - - None: if OpenPype is not found. - - Todo: - implement git/url support as OpenPype location, so it would be - possible to enter git url, OpenPype would check it out and if it is - ok install it as normal version. - - """ - if openpype_path and not isinstance(openpype_path, Path): - raise NotImplementedError( - ("Finding OpenPype in non-filesystem locations is" - " not implemented yet.")) - - dir_to_search = self.data_dir - user_versions = self.get_openpype_versions(self.data_dir, staging) - # if we have openpype_path specified, search only there. if openpype_path: - dir_to_search = openpype_path + openpype_versions = OpenPypeVersion.get_versions_from_directory( + openpype_path) + # filter out staging + + openpype_versions = [ + v for v in openpype_versions if v.is_staging() == staging + ] + else: - if os.getenv("OPENPYPE_PATH"): - if Path(os.getenv("OPENPYPE_PATH")).exists(): - dir_to_search = Path(os.getenv("OPENPYPE_PATH")) - else: - try: - registry_dir = Path( - str(self.registry.get_item("openPypePath"))) - if registry_dir.exists(): - dir_to_search = registry_dir - - except ValueError: - # nothing found in registry, we'll use data dir - pass - - openpype_versions = self.get_openpype_versions(dir_to_search, staging) - openpype_versions += user_versions + openpype_versions = OpenPypeVersion.get_available_versions(staging) # remove zip file version if needed. if not include_zips: diff --git a/start.py b/start.py index 0f7e82071d..05b7da6308 100644 --- a/start.py +++ b/start.py @@ -966,7 +966,6 @@ def boot(): ) sys.exit(1) - if not openpype_path: _print("*** Cannot get OpenPype path from database.") diff --git a/tests/unit/igniter/test_bootstrap_repos.py b/tests/unit/igniter/test_bootstrap_repos.py index d6e861c262..65cd5a2399 100644 --- a/tests/unit/igniter/test_bootstrap_repos.py +++ b/tests/unit/igniter/test_bootstrap_repos.py @@ -140,9 +140,10 @@ def test_search_string_for_openpype_version(printer): ] for ver_string in strings: printer(f"testing {ver_string[0]} should be {ver_string[1]}") - assert OpenPypeVersion.version_in_str(ver_string[0]) == \ - ver_string[1] - + assert isinstance( + OpenPypeVersion.version_in_str(ver_string[0]), + OpenPypeVersion if ver_string[1] else type(None) + ) @pytest.mark.slow def test_install_live_repos(fix_bootstrap, printer, monkeypatch, pytestconfig): From 88218caf2869d8452b734ed4712fa224a540b0c6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 15:56:11 +0100 Subject: [PATCH 003/105] store OpenPypeVersion to sys.modules --- igniter/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index defd45e233..bbc3dbfc88 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -6,9 +6,15 @@ import sys os.chdir(os.path.dirname(__file__)) # for override sys.path in Deadline -from .bootstrap_repos import BootstrapRepos +from .bootstrap_repos import ( + BootstrapRepos, + OpenPypeVersion +) from .version import __version__ as version +if "OpenPypeVersion" not in sys.modules: + sys.modules["OpenPypeVersion"] = OpenPypeVersion + def open_dialog(): """Show Igniter dialog.""" From 2bfdc5c3e970b1747e39b3ed16f439fe4d0e59d2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 16:05:36 +0100 Subject: [PATCH 004/105] create new enum entities used to determine openpype version --- openpype/settings/entities/__init__.py | 6 ++- openpype/settings/entities/enum_entity.py | 52 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index ccf2a5993e..e4a13b8053 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -106,7 +106,9 @@ from .enum_entity import ( ToolsEnumEntity, TaskTypeEnumEntity, DeadlineUrlEnumEntity, - AnatomyTemplatesEnumEntity + AnatomyTemplatesEnumEntity, + ProductionVersionsEnumEntity, + StagingVersionsEnumEntity ) from .list_entity import ListEntity @@ -169,6 +171,8 @@ __all__ = ( "TaskTypeEnumEntity", "DeadlineUrlEnumEntity", "AnatomyTemplatesEnumEntity", + "ProductionVersionsEnumEntity", + "StagingVersionsEnumEntity", "ListEntity", diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index ab3cebbd42..5f0cbb1261 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,4 +1,5 @@ import copy +import sys from .input_entities import InputEntity from .exceptions import EntitySchemaError from .lib import ( @@ -564,3 +565,54 @@ class AnatomyTemplatesEnumEntity(BaseEnumEntity): self.enum_items, self.valid_keys = self._get_enum_values() if self._current_value not in self.valid_keys: self._current_value = self.value_on_not_set + + +class _OpenPypeVersionEnum(BaseEnumEntity): + def _item_initialization(self): + self.multiselection = False + self.valid_value_types = (STRING_TYPE, ) + + self.value_on_not_set = "" + items = [ + {"": "Latest"} + ] + items.extend(self._get_openpype_versions()) + + self.enum_items = items + self.valid_keys = { + tuple(item.keys())[0] + for item in items + } + + def _get_openpype_versions(self): + return [] + + +class ProductionVersionsEnumEntity(_OpenPypeVersionEnum): + schema_types = ["production-versions-enum"] + + def _get_openpype_versions(self): + items = [] + if "OpenPypeVersion" in sys.modules: + OpenPypeVersion = sys.modules["OpenPypeVersion"] + versions = OpenPypeVersion.get_available_versions( + staging=False, local=False + ) + for item in versions: + items.append({item: item}) + return items + + +class StagingVersionsEnumEntity(_OpenPypeVersionEnum): + schema_types = ["staging-versions-enum"] + + def _get_openpype_versions(self): + items = [] + if "OpenPypeVersion" in sys.modules: + OpenPypeVersion = sys.modules["OpenPypeVersion"] + versions = OpenPypeVersion.get_available_versions( + staging=False, local=False + ) + for item in versions: + items.append({item: item}) + return items From 27b1be9279152b7fde4c3fbd5bb9d2d507cfc299 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 16:05:49 +0100 Subject: [PATCH 005/105] use new enities in settings --- .../settings/defaults/system_settings/general.json | 2 ++ .../schemas/system_schema/schema_general.json | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index f54e8b2b16..a07152eaf8 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -2,6 +2,8 @@ "studio_name": "Studio name", "studio_code": "stu", "admin_password": "", + "production_version": "", + "staging_version": "", "environment": { "__environment_keys__": { "global": [] diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index 51a58a6e27..d548a22c97 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -30,6 +30,19 @@ { "type": "splitter" }, + { + "type": "production-versions-enum", + "key": "production_version", + "label": "Production version" + }, + { + "type": "staging-versions-enum", + "key": "staging_version", + "label": "Staging version" + }, + { + "type": "splitter" + }, { "key": "environment", "label": "Environment", From ab8dacd0f8cc4e7f578695a6a5159472876ea1aa Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 16:06:04 +0100 Subject: [PATCH 006/105] added "production_version" and "staging_version" to global settings --- openpype/settings/handlers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index c59e2bc542..51e390bb6d 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -168,7 +168,13 @@ class CacheValues: class MongoSettingsHandler(SettingsHandler): """Settings handler that use mongo for storing and loading of settings.""" - global_general_keys = ("openpype_path", "admin_password", "disk_mapping") + global_general_keys = ( + "openpype_path", + "admin_password", + "disk_mapping", + "production_version", + "staging_version" + ) def __init__(self): # Get mongo connection From 80d887f14e80a1c2862343f1407280c1341a44ec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 17:44:49 +0100 Subject: [PATCH 007/105] added 3 method related to openpype path --- igniter/bootstrap_repos.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index ca64d193c7..921557b1a0 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -346,6 +346,37 @@ class OpenPypeVersion(semver.VersionInfo): return version['__version__'] + @classmethod + def get_openpype_path(cls): + """Path to openpype zip directory. + + Path can be set through environment variable 'OPENPYPE_PATH' which + is set during start of OpenPype if is not available. + """ + return os.getenv("OPENPYPE_PATH") + + @classmethod + def openpype_path_is_set(cls): + """Path to OpenPype zip directory is set.""" + if cls.get_openpype_path(): + return True + return False + + @classmethod + def openpype_path_is_accessible(cls): + """Path to OpenPype zip directory is accessible. + + Exists for this machine. + """ + # First check if is set + if not cls.openpype_path_is_set(): + return False + + # Validate existence + if Path(cls.get_openpype_path()).exists(): + return True + return False + @staticmethod def get_available_versions( staging: bool = False, local: bool = False) -> List: From 494bd26d80e41e0169344442ae2b773642e4191f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 18:04:18 +0100 Subject: [PATCH 008/105] created OpenPypeVersion wrapper in openpype lib --- openpype/lib/openpype_version.py | 59 ++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 openpype/lib/openpype_version.py diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py new file mode 100644 index 0000000000..4d1a1a7bb8 --- /dev/null +++ b/openpype/lib/openpype_version.py @@ -0,0 +1,59 @@ +"""Lib access to OpenPypeVersion from igniter. + +Access to logic from igniter is available only for OpenPype processes. +Is meant to be able check OpenPype versions for studio. The logic is dependent +on igniter's logic of processing. +""" + +import sys + + +def get_OpenPypeVersion(): + """Access to OpenPypeVersion class stored in sys modules.""" + return sys.modules.get("OpenPypeVersion") + + +def op_version_control_available(): + """Check if current process has access to OpenPypeVersion.""" + if get_OpenPypeVersion() is None: + return False + return True + + +def get_available_versions(*args, **kwargs): + """Get list of available versions.""" + if op_version_control_available(): + return get_OpenPypeVersion().get_available_versions( + *args, **kwargs + ) + return None + + +def openpype_path_is_set(): + if op_version_control_available(): + return get_OpenPypeVersion().openpype_path_is_set() + return None + + +def openpype_path_is_accessible(): + if op_version_control_available(): + return get_OpenPypeVersion().openpype_path_is_accessible() + return None + + +def get_latest_version(): + if op_version_control_available(): + return get_OpenPypeVersion().get_latest_version() + return None + + +def get_production_version(): + if op_version_control_available(): + return get_OpenPypeVersion().get_production_version() + return None + + +def get_staging_version(): + if op_version_control_available(): + return get_OpenPypeVersion().get_staging_version() + return None From cc1cc49235f288ba95d8d213e5fe16a8c67d397f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 18:06:52 +0100 Subject: [PATCH 009/105] added logic to load data for version entities --- openpype/settings/entities/enum_entity.py | 75 ++++++++++++++++++----- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 5f0cbb1261..591b2dd94f 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,8 +1,15 @@ import copy import sys +from openpype.lib.openpype_version import ( + op_version_control_available, + get_available_versions, + openpype_path_is_set, + openpype_path_is_accessible +) from .input_entities import InputEntity from .exceptions import EntitySchemaError from .lib import ( + OverrideState, NOT_SET, STRING_TYPE ) @@ -573,46 +580,84 @@ class _OpenPypeVersionEnum(BaseEnumEntity): self.valid_value_types = (STRING_TYPE, ) self.value_on_not_set = "" - items = [ - {"": "Latest"} - ] - items.extend(self._get_openpype_versions()) + items = self._get_default_items() self.enum_items = items - self.valid_keys = { + self.valid_keys = self._extract_valid_keys(items) + + def _extract_valid_keys(self, items): + return { tuple(item.keys())[0] for item in items } + def _get_default_items(self): + return [ + {"": "Latest"} + ] + def _get_openpype_versions(self): return [] + def set_override_state(self, state, *args, **kwargs): + items = self._get_default_items() + versions = self._get_openpype_versions() + if versions is not None: + for version in versions: + items.append({version: version}) + + self.enum_items = items + self.valid_keys = self._extract_valid_keys(items) + + # Studio value is not available in collected versions + if ( + state is OverrideState.STUDIO + and self.had_studio_override + and self._studio_override_value not in self.valid_keys + ): + # Define if entity should keep the value in settings. + # Value is marked as not existing anymore if + # - openpype version control is available + # - path to openpype zips is set + # - path to openpype zips is accessible (existing for this machine) + keep_value = True + if ( + op_version_control_available() + and openpype_path_is_set() + and openpype_path_is_accessible() + ): + keep_value = False + + if keep_value: + self.enum_items.append( + {self._studio_override_value: self._studio_override_value} + ) + self.valid_keys.add(self._studio_override_value) + + super(_OpenPypeVersionEnum, self).set_override_state( + state, *args, **kwargs + ) + class ProductionVersionsEnumEntity(_OpenPypeVersionEnum): schema_types = ["production-versions-enum"] def _get_openpype_versions(self): - items = [] if "OpenPypeVersion" in sys.modules: OpenPypeVersion = sys.modules["OpenPypeVersion"] - versions = OpenPypeVersion.get_available_versions( + return get_available_versions( staging=False, local=False ) - for item in versions: - items.append({item: item}) - return items + return None class StagingVersionsEnumEntity(_OpenPypeVersionEnum): schema_types = ["staging-versions-enum"] def _get_openpype_versions(self): - items = [] if "OpenPypeVersion" in sys.modules: OpenPypeVersion = sys.modules["OpenPypeVersion"] - versions = OpenPypeVersion.get_available_versions( + return get_available_versions( staging=False, local=False ) - for item in versions: - items.append({item: item}) - return items + return None From e9b0347d8309705da17a57463f59a95bb8d1a042 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 18:26:30 +0100 Subject: [PATCH 010/105] changed static method to class method --- igniter/bootstrap_repos.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 921557b1a0..5cade8324c 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -377,9 +377,9 @@ class OpenPypeVersion(semver.VersionInfo): return True return False - @staticmethod + @classmethod def get_available_versions( - staging: bool = False, local: bool = False) -> List: + cls, staging: bool = False, local: bool = False) -> List: """Get ordered dict of detected OpenPype version. Resolution order for OpenPype is following: From 6762d6645957dc177796a067c64856fafe6096b2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 18:26:54 +0100 Subject: [PATCH 011/105] added functions to retrieve local and remote versions separatelly --- igniter/bootstrap_repos.py | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 5cade8324c..c52175c8fd 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -377,6 +377,90 @@ class OpenPypeVersion(semver.VersionInfo): return True return False + @classmethod + def get_local_versions( + cls, production: bool = None, staging: bool = None + ) -> List: + """Get all versions available on this machine. + + Arguments give ability to specify if filtering is needed. If both + arguments are set to None all found versions are returned. + + Args: + production (bool): Return production versions. + staging (bool): Return staging versions. + """ + # Return all local versions if arguments are set to None + if production is None and staging is None: + production = True + staging = True + + # Just return empty output if both are disabled + elif not production and not staging: + return [] + + dir_to_search = Path(user_data_dir("openpype", "pypeclub")) + versions = OpenPypeVersion.get_versions_from_directory( + dir_to_search + ) + filtered_versions = [] + for version in versions: + if version.is_staging(): + if staging: + filtered_versions.append(version) + elif production: + filtered_versions.append(version) + return list(sorted(set(filtered_versions))) + + @classmethod + def get_remote_versions( + cls, production: bool = None, staging: bool = None + ) -> List: + """Get all versions available in OpenPype Path. + + Arguments give ability to specify if filtering is needed. If both + arguments are set to None all found versions are returned. + + Args: + production (bool): Return production versions. + staging (bool): Return staging versions. + """ + # Return all local versions if arguments are set to None + if production is None and staging is None: + production = True + staging = True + + # Just return empty output if both are disabled + elif not production and not staging: + return [] + + dir_to_search = None + if cls.openpype_path_is_accessible(): + dir_to_search = Path(cls.get_openpype_path()) + else: + registry = OpenPypeSettingsRegistry() + try: + registry_dir = Path(str(registry.get_item("openPypePath"))) + if registry_dir.exists(): + dir_to_search = registry_dir + + except ValueError: + # nothing found in registry, we'll use data dir + pass + + if not dir_to_search: + return [] + + versions = cls.get_versions_from_directory(dir_to_search) + filtered_versions = [] + for version in versions: + if version.is_staging(): + if staging: + filtered_versions.append(version) + elif production: + filtered_versions.append(version) + return list(sorted(set(filtered_versions))) + @classmethod def get_available_versions( cls, staging: bool = False, local: bool = False) -> List: From 73f29819873c632111a3a2b3a644afc36f04509a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 18:27:14 +0100 Subject: [PATCH 012/105] use new functions in get_available_versions --- igniter/bootstrap_repos.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index c52175c8fd..5bb7296d3d 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -479,29 +479,11 @@ class OpenPypeVersion(semver.VersionInfo): local (bool, optional): List only local versions. """ - registry = OpenPypeSettingsRegistry() - dir_to_search = Path(user_data_dir("openpype", "pypeclub")) - user_versions = OpenPypeVersion.get_versions_from_directory( - dir_to_search) + user_versions = cls.get_local_versions() # if we have openpype_path specified, search only there. - + openpype_versions = [] if not local: - if os.getenv("OPENPYPE_PATH"): - if Path(os.getenv("OPENPYPE_PATH")).exists(): - dir_to_search = Path(os.getenv("OPENPYPE_PATH")) - else: - try: - registry_dir = Path( - str(registry.get_item("openPypePath"))) - if registry_dir.exists(): - dir_to_search = registry_dir - - except ValueError: - # nothing found in registry, we'll use data dir - pass - - openpype_versions = OpenPypeVersion.get_versions_from_directory( - dir_to_search) + openpype_versions = cls.get_remote_versions() openpype_versions += user_versions # remove duplicates and staging/production From a14662bccee58dd50faeb887842889ae7dc63ff1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 18:35:21 +0100 Subject: [PATCH 013/105] fix args conditions --- igniter/bootstrap_repos.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 5bb7296d3d..94f786e869 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -395,8 +395,14 @@ class OpenPypeVersion(semver.VersionInfo): production = True staging = True + elif production is None and not staging: + production = True + + elif staging is None and not production: + staging = True + # Just return empty output if both are disabled - elif not production and not staging: + if not production and not staging: return [] dir_to_search = Path(user_data_dir("openpype", "pypeclub")) @@ -430,8 +436,14 @@ class OpenPypeVersion(semver.VersionInfo): production = True staging = True + elif production is None and not staging: + production = True + + elif staging is None and not production: + staging = True + # Just return empty output if both are disabled - elif not production and not staging: + if not production and not staging: return [] dir_to_search = None From 1567afc4b9ba205669f3e716a96efb8963c5e79c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 18:35:34 +0100 Subject: [PATCH 014/105] extended available functions --- openpype/lib/openpype_version.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 4d1a1a7bb8..42ee454378 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -41,19 +41,31 @@ def openpype_path_is_accessible(): return None -def get_latest_version(): +def get_local_versions(*args, **kwargs): if op_version_control_available(): - return get_OpenPypeVersion().get_latest_version() + return get_OpenPypeVersion().get_local_versions(*args, **kwargs) return None -def get_production_version(): +def get_remote_versions(*args, **kwargs): + if op_version_control_available(): + return get_OpenPypeVersion().get_remote_versions(*args, **kwargs) + return None + + +def get_latest_version(*args, **kwargs): + if op_version_control_available(): + return get_OpenPypeVersion().get_latest_version(*args, **kwargs) + return None + + +def get_current_production_version(): if op_version_control_available(): return get_OpenPypeVersion().get_production_version() return None -def get_staging_version(): +def get_current_staging_version(): if op_version_control_available(): return get_OpenPypeVersion().get_staging_version() return None From 7aec21aa800dc225fbc5d2db9f46e8defcd26f29 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Dec 2021 18:35:45 +0100 Subject: [PATCH 015/105] use extended functions in entities --- openpype/settings/entities/enum_entity.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 591b2dd94f..534775a41d 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -2,7 +2,7 @@ import copy import sys from openpype.lib.openpype_version import ( op_version_control_available, - get_available_versions, + get_remote_versions, openpype_path_is_set, openpype_path_is_accessible ) @@ -645,9 +645,7 @@ class ProductionVersionsEnumEntity(_OpenPypeVersionEnum): def _get_openpype_versions(self): if "OpenPypeVersion" in sys.modules: OpenPypeVersion = sys.modules["OpenPypeVersion"] - return get_available_versions( - staging=False, local=False - ) + return get_remote_versions(production=True) return None @@ -657,7 +655,5 @@ class StagingVersionsEnumEntity(_OpenPypeVersionEnum): def _get_openpype_versions(self): if "OpenPypeVersion" in sys.modules: OpenPypeVersion = sys.modules["OpenPypeVersion"] - return get_available_versions( - staging=False, local=False - ) + return get_remote_versions(staging=True) return None From 8f0f0769800645e4966fc1e301ce3c1e97bde925 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 11:51:35 +0100 Subject: [PATCH 016/105] added functions to get expected studio version --- igniter/bootstrap_repos.py | 23 ++++++++++++++++++++++- igniter/tools.py | 18 ++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 94f786e869..9446b3e8ce 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -22,7 +22,10 @@ from .user_settings import ( OpenPypeSecureRegistry, OpenPypeSettingsRegistry ) -from .tools import get_openpype_path_from_db +from .tools import ( + get_openpype_path_from_db, + get_expected_studio_version_str +) LOG_INFO = 0 @@ -571,6 +574,24 @@ class OpenPypeVersion(semver.VersionInfo): return openpype_versions[-1] + @classmethod + def get_expected_studio_version(cls, staging=False): + """Expected OpenPype version that should be used at the moment. + + If version is not defined in settings the latest found version is + used. + + Args: + staging (bool): Staging version or production version. + + Returns: + OpenPypeVersion: Version that should be used. + """ + result = get_expected_studio_version_str(staging) + if not result: + return cls.get_latest_version(staging, False) + return OpenPypeVersion(version=result) + class BootstrapRepos: """Class for bootstrapping local OpenPype installation. diff --git a/igniter/tools.py b/igniter/tools.py index 3e862f5803..5cad2b9bf8 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -182,6 +182,24 @@ def get_openpype_path_from_db(url: str) -> Union[str, None]: return None +def get_expected_studio_version_str(staging=False) -> str: + """Version that should be currently used in studio. + + Args: + staging (bool): Get current version for staging. + + Returns: + str: OpenPype version which should be used. Empty string means latest. + """ + mongo_url = os.environ.get("OPENPYPE_MONGO") + global_settings = get_openpype_global_settings(mongo_url) + if staging: + key = "staging_version" + else: + key = "production_version" + return global_settings.get(key) or "" + + def load_stylesheet() -> str: """Load css style sheet. From 12498862943b12ba7c039ba19b71d58fa7e85de5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:10:29 +0100 Subject: [PATCH 017/105] text entity may have value hints --- openpype/settings/entities/input_entities.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index a0598d405e..d45e8f9f01 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -438,6 +438,7 @@ class TextEntity(InputEntity): # GUI attributes self.multiline = self.schema_data.get("multiline", False) self.placeholder_text = self.schema_data.get("placeholder") + self.value_hints = self.schema_data.get("value_hints") or [] def _convert_to_valid_type(self, value): # Allow numbers converted to string From 376c4f977864a285b2ca2f271e1cf9f4e5571589 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:10:49 +0100 Subject: [PATCH 018/105] changed openpype version enums to text inputs --- openpype/settings/entities/__init__.py | 14 +-- openpype/settings/entities/enum_entity.py | 87 ------------------- .../settings/entities/op_version_entity.py | 49 +++++++++++ .../schemas/system_schema/schema_general.json | 4 +- 4 files changed, 59 insertions(+), 95 deletions(-) create mode 100644 openpype/settings/entities/op_version_entity.py diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index e4a13b8053..4efd358297 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -107,8 +107,6 @@ from .enum_entity import ( TaskTypeEnumEntity, DeadlineUrlEnumEntity, AnatomyTemplatesEnumEntity, - ProductionVersionsEnumEntity, - StagingVersionsEnumEntity ) from .list_entity import ListEntity @@ -124,7 +122,10 @@ from .dict_conditional import ( ) from .anatomy_entities import AnatomyEntity - +from .op_version_entity import ( + ProductionVersionsInputEntity, + StagingVersionsInputEntity +) __all__ = ( "DefaultsNotDefined", @@ -171,8 +172,6 @@ __all__ = ( "TaskTypeEnumEntity", "DeadlineUrlEnumEntity", "AnatomyTemplatesEnumEntity", - "ProductionVersionsEnumEntity", - "StagingVersionsEnumEntity", "ListEntity", @@ -185,5 +184,8 @@ __all__ = ( "DictConditionalEntity", "SyncServerProviders", - "AnatomyEntity" + "AnatomyEntity", + + "ProductionVersionsInputEntity", + "StagingVersionsInputEntity" ) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 534775a41d..1f9b361f16 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,5 +1,4 @@ import copy -import sys from openpype.lib.openpype_version import ( op_version_control_available, get_remote_versions, @@ -9,7 +8,6 @@ from openpype.lib.openpype_version import ( from .input_entities import InputEntity from .exceptions import EntitySchemaError from .lib import ( - OverrideState, NOT_SET, STRING_TYPE ) @@ -572,88 +570,3 @@ class AnatomyTemplatesEnumEntity(BaseEnumEntity): self.enum_items, self.valid_keys = self._get_enum_values() if self._current_value not in self.valid_keys: self._current_value = self.value_on_not_set - - -class _OpenPypeVersionEnum(BaseEnumEntity): - def _item_initialization(self): - self.multiselection = False - self.valid_value_types = (STRING_TYPE, ) - - self.value_on_not_set = "" - items = self._get_default_items() - - self.enum_items = items - self.valid_keys = self._extract_valid_keys(items) - - def _extract_valid_keys(self, items): - return { - tuple(item.keys())[0] - for item in items - } - - def _get_default_items(self): - return [ - {"": "Latest"} - ] - - def _get_openpype_versions(self): - return [] - - def set_override_state(self, state, *args, **kwargs): - items = self._get_default_items() - versions = self._get_openpype_versions() - if versions is not None: - for version in versions: - items.append({version: version}) - - self.enum_items = items - self.valid_keys = self._extract_valid_keys(items) - - # Studio value is not available in collected versions - if ( - state is OverrideState.STUDIO - and self.had_studio_override - and self._studio_override_value not in self.valid_keys - ): - # Define if entity should keep the value in settings. - # Value is marked as not existing anymore if - # - openpype version control is available - # - path to openpype zips is set - # - path to openpype zips is accessible (existing for this machine) - keep_value = True - if ( - op_version_control_available() - and openpype_path_is_set() - and openpype_path_is_accessible() - ): - keep_value = False - - if keep_value: - self.enum_items.append( - {self._studio_override_value: self._studio_override_value} - ) - self.valid_keys.add(self._studio_override_value) - - super(_OpenPypeVersionEnum, self).set_override_state( - state, *args, **kwargs - ) - - -class ProductionVersionsEnumEntity(_OpenPypeVersionEnum): - schema_types = ["production-versions-enum"] - - def _get_openpype_versions(self): - if "OpenPypeVersion" in sys.modules: - OpenPypeVersion = sys.modules["OpenPypeVersion"] - return get_remote_versions(production=True) - return None - - -class StagingVersionsEnumEntity(_OpenPypeVersionEnum): - schema_types = ["staging-versions-enum"] - - def _get_openpype_versions(self): - if "OpenPypeVersion" in sys.modules: - OpenPypeVersion = sys.modules["OpenPypeVersion"] - return get_remote_versions(staging=True) - return None diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py new file mode 100644 index 0000000000..e00c6a4737 --- /dev/null +++ b/openpype/settings/entities/op_version_entity.py @@ -0,0 +1,49 @@ +from openpype.lib.openpype_version import ( + op_version_control_available, + get_remote_versions, + openpype_path_is_set, + openpype_path_is_accessible +) +from .input_entities import TextEntity +from .lib import OverrideState + + +class _OpenPypeVersionInput(TextEntity): + def _item_initialization(self): + super(_OpenPypeVersionInput, self)._item_initialization() + self.multiline = False + self.placeholder_text = "Latest" + self.value_hints = [] + + def _get_openpype_versions(self): + return [] + + def set_override_state(self, state, *args, **kwargs): + value_hints = [] + if state is OverrideState.STUDIO: + versions = self._get_openpype_versions() + if versions is not None: + for version in versions: + value_hints.append(str(version)) + + self.value_hints = value_hints + + super(_OpenPypeVersionInput, self).set_override_state( + state, *args, **kwargs + ) + + +class ProductionVersionsInputEntity(_OpenPypeVersionInput): + schema_types = ["production-versions-text"] + + def _get_openpype_versions(self): + return ["", "asd", "dsa", "3.6"] + return get_remote_versions(production=True) + + +class StagingVersionsInputEntity(_OpenPypeVersionInput): + schema_types = ["staging-versions-text"] + + def _get_openpype_versions(self): + return ["", "asd+staging", "dsa+staging", "3.6+staging"] + return get_remote_versions(staging=True) diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index d548a22c97..b848a34dda 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -31,12 +31,12 @@ "type": "splitter" }, { - "type": "production-versions-enum", + "type": "production-versions-text", "key": "production_version", "label": "Production version" }, { - "type": "staging-versions-enum", + "type": "staging-versions-text", "key": "staging_version", "label": "Staging version" }, From d856c4602b712d6619883dfb7da819eb2d01dd84 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:11:26 +0100 Subject: [PATCH 019/105] added completer implementation for value hints --- openpype/tools/settings/settings/widgets.py | 220 ++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 7a7213fa66..bf59f605c9 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -1,4 +1,5 @@ import os +import copy from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome from avalon.mongodb import ( @@ -24,13 +25,232 @@ from .constants import ( ) +class CompleterFilter(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(CompleterFilter, self).__init__(*args, **kwargs) + + self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + + self._text_filter = "" + + def set_text_filter(self, text): + if self._text_filter == text: + return + self._text_filter = text + self.invalidateFilter() + + def filterAcceptsRow(self, row, parent_index): + if not self._text_filter: + return True + model = self.sourceModel() + index = model.index(row, self.filterKeyColumn(), parent_index) + value = index.data(QtCore.Qt.DisplayRole) + if self._text_filter in value: + if self._text_filter == value: + return False + return True + return False + + +class CompleterView(QtWidgets.QListView): + row_activated = QtCore.Signal(str) + + def __init__(self, parent): + super(CompleterView, self).__init__(parent) + + self.setWindowFlags( + QtCore.Qt.FramelessWindowHint + | QtCore.Qt.Tool + ) + delegate = QtWidgets.QStyledItemDelegate() + self.setItemDelegate(delegate) + + model = QtGui.QStandardItemModel() + filter_model = CompleterFilter() + filter_model.setSourceModel(model) + self.setModel(filter_model) + + # self.installEventFilter(parent) + + self.clicked.connect(self._on_activated) + + self._last_loaded_values = None + self._model = model + self._filter_model = filter_model + self._delegate = delegate + + def _on_activated(self, index): + if index.isValid(): + value = index.data(QtCore.Qt.DisplayRole) + self.row_activated.emit(value) + + def set_text_filter(self, text): + self._filter_model.set_text_filter(text) + self._update_geo() + + def sizeHint(self): + result = super(CompleterView, self).sizeHint() + height = 0 + for row in range(self._filter_model.rowCount()): + height += self.sizeHintForRow(row) + result.setHeight(height) + return result + + def _update_geo(self): + size_hint = self.sizeHint() + self.resize(size_hint.width(), size_hint.height()) + + def update_values(self, values): + if not values: + values = [] + + if self._last_loaded_values == values: + return + self._last_loaded_values = copy.deepcopy(values) + + root_item = self._model.invisibleRootItem() + existing_values = {} + for row in reversed(range(root_item.rowCount())): + child = root_item.child(row) + value = child.data(QtCore.Qt.DisplayRole) + if value not in values: + root_item.removeRows(child.row()) + else: + existing_values[value] = child + + for row, value in enumerate(values): + if value in existing_values: + item = existing_values[value] + if item.row() == row: + continue + else: + item = QtGui.QStandardItem(value) + item.setEditable(False) + + root_item.setChild(row, item) + + self._update_geo() + + def _get_selected_row(self): + indexes = self.selectionModel().selectedIndexes() + if not indexes: + return -1 + return indexes[0].row() + + def _select_row(self, row): + index = self._filter_model.index(row, 0) + self.setCurrentIndex(index) + + def move_up(self): + rows = self._filter_model.rowCount() + if rows == 0: + return + + selected_row = self._get_selected_row() + if selected_row < 0: + new_row = rows - 1 + else: + new_row = selected_row - 1 + if new_row < 0: + new_row = rows - 1 + + if new_row != selected_row: + self._select_row(new_row) + + def move_down(self): + rows = self._filter_model.rowCount() + if rows == 0: + return + + selected_row = self._get_selected_row() + if selected_row < 0: + new_row = 0 + else: + new_row = selected_row + 1 + if new_row >= rows: + new_row = 0 + + if new_row != selected_row: + self._select_row(new_row) + + def enter_pressed(self): + selected_row = self._get_selected_row() + if selected_row < 0: + return + index = self._filter_model.index(selected_row, 0) + self._on_activated(index) + + class SettingsLineEdit(QtWidgets.QLineEdit): focused_in = QtCore.Signal() + def __init__(self, *args, **kwargs): + super(SettingsLineEdit, self).__init__(*args, **kwargs) + + self._completer = None + + self.textChanged.connect(self._on_text_change) + + def _on_text_change(self, text): + if self._completer is not None: + self._completer.set_text_filter(text) + + def _update_completer(self): + if self._completer is None or not self._completer.isVisible(): + return + point = self.frameGeometry().bottomLeft() + new_point = self.mapToGlobal(point) + self._completer.move(new_point) + def focusInEvent(self, event): super(SettingsLineEdit, self).focusInEvent(event) self.focused_in.emit() + if self._completer is None: + return + self._completer.show() + self._update_completer() + + def focusOutEvent(self, event): + super(SettingsLineEdit, self).focusOutEvent(event) + if self._completer is not None: + self._completer.hide() + + def paintEvent(self, event): + super(SettingsLineEdit, self).paintEvent(event) + self._update_completer() + + def update_completer_values(self, values): + if not values and self._completer is None: + return + + self._create_completer() + + self._completer.update_values(values) + + def _create_completer(self): + if self._completer is None: + self._completer = CompleterView(self) + self._completer.row_activated.connect(self._completer_activated) + + def _completer_activated(self, text): + self.setText(text) + + def keyPressEvent(self, event): + if self._completer is None: + super(SettingsLineEdit, self).keyPressEvent(event) + return + + key = event.key() + if key == QtCore.Qt.Key_Up: + self._completer.move_up() + elif key == QtCore.Qt.Key_Down: + self._completer.move_down() + elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return): + self._completer.enter_pressed() + else: + super(SettingsLineEdit, self).keyPressEvent(event) + class SettingsPlainTextEdit(QtWidgets.QPlainTextEdit): focused_in = QtCore.Signal() From 4c2ccb014c3c2a1c815638b5e9f90c86fd36c5cb Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:12:17 +0100 Subject: [PATCH 020/105] text input fills value hints --- openpype/tools/settings/settings/item_widgets.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 2e00967a60..f20deeb6e4 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -2,6 +2,9 @@ import json from Qt import QtWidgets, QtCore, QtGui +from openpype.widgets.sliders import NiceSlider +from openpype.tools.settings import CHILD_OFFSET + from .widgets import ( ExpandingWidget, NumberSpinBox, @@ -22,9 +25,6 @@ from .base import ( InputWidget ) -from openpype.widgets.sliders import NiceSlider -from openpype.tools.settings import CHILD_OFFSET - class DictImmutableKeysWidget(BaseWidget): def create_ui(self): @@ -378,6 +378,11 @@ class TextWidget(InputWidget): self.input_field.focused_in.connect(self._on_input_focus) self.input_field.textChanged.connect(self._on_value_change) + self._refresh_completer() + + def _refresh_completer(self): + self.input_field.update_completer_values(self.entity.value_hints) + def _on_input_focus(self): self.focused_in() From d7ad673462547ef8c653e2041e71b04594a31d3f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:15:26 +0100 Subject: [PATCH 021/105] removed test data --- openpype/settings/entities/op_version_entity.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index e00c6a4737..a6954119aa 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -37,7 +37,6 @@ class ProductionVersionsInputEntity(_OpenPypeVersionInput): schema_types = ["production-versions-text"] def _get_openpype_versions(self): - return ["", "asd", "dsa", "3.6"] return get_remote_versions(production=True) @@ -45,5 +44,4 @@ class StagingVersionsInputEntity(_OpenPypeVersionInput): schema_types = ["staging-versions-text"] def _get_openpype_versions(self): - return ["", "asd+staging", "dsa+staging", "3.6+staging"] return get_remote_versions(staging=True) From fc9a50f7423ecf7e684396313a1679e2f3d07b44 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:15:54 +0100 Subject: [PATCH 022/105] added special widget for openpype version input --- openpype/settings/entities/op_version_entity.py | 10 +++++----- openpype/tools/settings/settings/categories.py | 7 +++++++ openpype/tools/settings/settings/item_widgets.py | 7 +++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index a6954119aa..2458f03852 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -8,9 +8,9 @@ from .input_entities import TextEntity from .lib import OverrideState -class _OpenPypeVersionInput(TextEntity): +class OpenPypeVersionInput(TextEntity): def _item_initialization(self): - super(_OpenPypeVersionInput, self)._item_initialization() + super(OpenPypeVersionInput, self)._item_initialization() self.multiline = False self.placeholder_text = "Latest" self.value_hints = [] @@ -28,19 +28,19 @@ class _OpenPypeVersionInput(TextEntity): self.value_hints = value_hints - super(_OpenPypeVersionInput, self).set_override_state( + super(OpenPypeVersionInput, self).set_override_state( state, *args, **kwargs ) -class ProductionVersionsInputEntity(_OpenPypeVersionInput): +class ProductionVersionsInputEntity(OpenPypeVersionInput): schema_types = ["production-versions-text"] def _get_openpype_versions(self): return get_remote_versions(production=True) -class StagingVersionsInputEntity(_OpenPypeVersionInput): +class StagingVersionsInputEntity(OpenPypeVersionInput): schema_types = ["staging-versions-text"] def _get_openpype_versions(self): diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index a6e4154b2b..af7e0bd742 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -28,6 +28,9 @@ from openpype.settings.entities import ( StudioDefaultsNotDefined, SchemaError ) +from openpype.settings.entities.op_version_entity import ( + OpenPypeVersionInput +) from openpype.settings import SaveWarningExc from .widgets import ProjectListWidget @@ -46,6 +49,7 @@ from .item_widgets import ( BoolWidget, DictImmutableKeysWidget, TextWidget, + OpenPypeVersionText, NumberWidget, RawJsonWidget, EnumeratorWidget, @@ -116,6 +120,9 @@ class SettingsCategoryWidget(QtWidgets.QWidget): elif isinstance(entity, BoolEntity): return BoolWidget(*args) + elif isinstance(entity, OpenPypeVersionInput): + return OpenPypeVersionText(*args) + elif isinstance(entity, TextEntity): return TextWidget(*args) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index f20deeb6e4..11f0dc6add 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -389,6 +389,7 @@ class TextWidget(InputWidget): def _on_entity_change(self): if self.entity.value != self.input_value(): self.set_entity_value() + self._refresh_completer() def set_entity_value(self): if self.entity.multiline: @@ -411,6 +412,12 @@ class TextWidget(InputWidget): self.entity.set(self.input_value()) +class OpenPypeVersionText(TextWidget): + def _on_entity_change(self): + super(OpenPypeVersionText, self)._on_entity_change() + self._refresh_completer() + + class NumberWidget(InputWidget): _slider_widget = None From 8b4455721ed26a5bae3a4d74efe88d03a1854a45 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:22:01 +0100 Subject: [PATCH 023/105] removed overdone refresh of completer --- openpype/tools/settings/settings/item_widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 11f0dc6add..bca70edff5 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -389,7 +389,6 @@ class TextWidget(InputWidget): def _on_entity_change(self): if self.entity.value != self.input_value(): self.set_entity_value() - self._refresh_completer() def set_entity_value(self): if self.entity.multiline: From af12e59f826f3b27c3b6c8256650da1360fcffb2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:23:40 +0100 Subject: [PATCH 024/105] skip completer refresh for multiline entities --- openpype/tools/settings/settings/item_widgets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index bca70edff5..3a7e126846 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -378,9 +378,12 @@ class TextWidget(InputWidget): self.input_field.focused_in.connect(self._on_input_focus) self.input_field.textChanged.connect(self._on_value_change) - self._refresh_completer() - def _refresh_completer(self): + # Multiline entity can't have completer + # - there is not space for this UI component + if self.entity.multiline: + return + self.input_field.update_completer_values(self.entity.value_hints) def _on_input_focus(self): From 8e853314daac94ce4ecf9966ff85d72c75347a03 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:23:53 +0100 Subject: [PATCH 025/105] added schema validation for value hints --- openpype/settings/entities/input_entities.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index d45e8f9f01..a285bf3433 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -440,6 +440,15 @@ class TextEntity(InputEntity): self.placeholder_text = self.schema_data.get("placeholder") self.value_hints = self.schema_data.get("value_hints") or [] + def schema_validations(self): + if self.multiline and self.value_hints: + reason = ( + "TextEntity entity can't use value hints" + " for multiline input (yet)." + ) + raise EntitySchemaError(self, reason) + super(TextEntity, self).schema_validations() + def _convert_to_valid_type(self, value): # Allow numbers converted to string if isinstance(value, (int, float)): From 298d2da581805e3f704c651bda6ed7e680d58bf4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 6 Dec 2021 18:57:36 +0100 Subject: [PATCH 026/105] added refresh of completer to init --- openpype/tools/settings/settings/item_widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 3a7e126846..1d912a90b7 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -378,6 +378,8 @@ class TextWidget(InputWidget): self.input_field.focused_in.connect(self._on_input_focus) self.input_field.textChanged.connect(self._on_value_change) + self._refresh_completer() + def _refresh_completer(self): # Multiline entity can't have completer # - there is not space for this UI component From c479fdecdb13085ed4d58c5c044ccf77587c3e76 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 10:36:13 +0100 Subject: [PATCH 027/105] added label --- .../entities/schemas/system_schema/schema_general.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index b848a34dda..ed66cd7ac8 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -30,6 +30,10 @@ { "type": "splitter" }, + { + "type": "label", + "label": "Define explicit OpenPype version that should be used. Keep empty to use latest available version." + }, { "type": "production-versions-text", "key": "production_version", From 1ea180ba8d58fdb4010eefad0739e84d3f1482e9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 11:21:48 +0100 Subject: [PATCH 028/105] removed dot --- .../settings/entities/schemas/system_schema/schema_general.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/system_schema/schema_general.json b/openpype/settings/entities/schemas/system_schema/schema_general.json index ed66cd7ac8..b4c83fc85f 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_general.json +++ b/openpype/settings/entities/schemas/system_schema/schema_general.json @@ -20,7 +20,7 @@ }, { "type": "label", - "label": "This is NOT a securely stored password!. It only acts as a simple barrier to stop users from accessing studio wide settings." + "label": "This is NOT a securely stored password! It only acts as a simple barrier to stop users from accessing studio wide settings." }, { "type": "text", From 1454efab3a0528ccaa8cbcb417773fafd8f10e2d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 13:43:51 +0100 Subject: [PATCH 029/105] added style for completer view --- openpype/style/style.css | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/style/style.css b/openpype/style/style.css index 19245cdc40..0ef15a2511 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -536,6 +536,27 @@ QAbstractItemView::branch:!has-children:!has-siblings:adjoins-item { background: transparent; } +CompleterView { + border: 1px solid #555555; + background: {color:bg-inputs}; +} + +CompleterView::item:selected { + background: {color:bg-view-hover}; +} + +CompleterView::item:selected:hover { + background: {color:bg-view-hover}; +} + +CompleterView::right-arrow { + min-width: 10px; +} +CompleterView::separator { + background: {color:bg-menu-separator}; + height: 2px; + margin-right: 5px; +} /* Progress bar */ QProgressBar { From 44e00ed1d442d3d9dc3cb0a68a8976fb63da401c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 13:44:02 +0100 Subject: [PATCH 030/105] don't care about ideal height --- openpype/tools/settings/settings/widgets.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index bf59f605c9..4b88c1f93f 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -90,10 +90,9 @@ class CompleterView(QtWidgets.QListView): def sizeHint(self): result = super(CompleterView, self).sizeHint() - height = 0 - for row in range(self._filter_model.rowCount()): - height += self.sizeHintForRow(row) - result.setHeight(height) + if self._filter_model.rowCount() == 0: + result.setHeight(0) + return result def _update_geo(self): From 228d6fc914f3f1e6bcd6212d323facb9b67d4364 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 13:45:21 +0100 Subject: [PATCH 031/105] added basic information about valid version --- .../tools/settings/settings/item_widgets.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 1d912a90b7..0c66162375 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -417,9 +417,30 @@ class TextWidget(InputWidget): class OpenPypeVersionText(TextWidget): + def __init__(self, *args, **kwargs): + self._info_widget = None + super(OpenPypeVersionText, self).__init__(*args, **kwargs) + + def create_ui(self): + super(OpenPypeVersionText, self).create_ui() + info_widget = QtWidgets.QLabel("Latest", self) + self.content_layout.addWidget(info_widget, 1) + + self._info_widget = info_widget + + def _update_info_widget(self): + value = self.input_value() + if value == "": + self._info_widget.setText("Latest") + elif value in self.entity.value_hints: + self._info_widget.setText("Ok") + else: + self._info_widget.setText("Version not found from this workstation") + def _on_entity_change(self): super(OpenPypeVersionText, self)._on_entity_change() self._refresh_completer() + self._update_info_widget() class NumberWidget(InputWidget): From 87b606582a95696f4a4fc3091c4cbd624538d0b7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:10:13 +0100 Subject: [PATCH 032/105] do not create new qapplication if already exists --- igniter/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index bbc3dbfc88..d974bd9e0d 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -28,7 +28,9 @@ def open_dialog(): if scale_attr is not None: QtWidgets.QApplication.setAttribute(scale_attr) - app = QtWidgets.QApplication(sys.argv) + app = QtWidgets.QApplication.instance() + if not app: + app = QtWidgets.QApplication(sys.argv) d = InstallDialog() d.open() @@ -49,7 +51,9 @@ def open_update_window(openpype_version): if scale_attr is not None: QtWidgets.QApplication.setAttribute(scale_attr) - app = QtWidgets.QApplication(sys.argv) + app = QtWidgets.QApplication.instance() + if not app: + app = QtWidgets.QApplication(sys.argv) d = UpdateWindow(version=openpype_version) d.open() From db467dcbfd041a7fdd60399a2535bf215e40a7c3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:13:56 +0100 Subject: [PATCH 033/105] added function to get openpype icon to tools.py --- igniter/install_dialog.py | 5 +++-- igniter/tools.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index 1fe67e3397..251adebc9f 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -12,7 +12,8 @@ from Qt.QtCore import QTimer # noqa from .install_thread import InstallThread from .tools import ( validate_mongo_connection, - get_openpype_path_from_db + get_openpype_path_from_db, + get_openpype_icon_path ) from .nice_progress_bar import NiceProgressBar @@ -187,7 +188,6 @@ class InstallDialog(QtWidgets.QDialog): current_dir = os.path.dirname(os.path.abspath(__file__)) roboto_font_path = os.path.join(current_dir, "RobotoMono-Regular.ttf") poppins_font_path = os.path.join(current_dir, "Poppins") - icon_path = os.path.join(current_dir, "openpype_icon.png") # Install roboto font QtGui.QFontDatabase.addApplicationFont(roboto_font_path) @@ -196,6 +196,7 @@ class InstallDialog(QtWidgets.QDialog): QtGui.QFontDatabase.addApplicationFont(filename) # Load logo + icon_path = get_openpype_icon_path() pixmap_openpype_logo = QtGui.QPixmap(icon_path) # Set logo as icon of window self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo)) diff --git a/igniter/tools.py b/igniter/tools.py index 5cad2b9bf8..72b98f1f82 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -210,3 +210,11 @@ def load_stylesheet() -> str: stylesheet_path = Path(__file__).parent.resolve() / "stylesheet.css" return stylesheet_path.read_text() + + +def get_openpype_icon_path() -> str: + """Path to OpenPype icon png file.""" + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "openpype_icon.png" + ) From e4534c5074244afa096bc290809011d0b24eb7a6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:21:05 +0100 Subject: [PATCH 034/105] added new message dialog to show a dialog --- igniter/__init__.py | 22 ++++++++++++++++++++ igniter/message_dialog.py | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 igniter/message_dialog.py diff --git a/igniter/__init__.py b/igniter/__init__.py index d974bd9e0d..6f06c1eb90 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -63,9 +63,31 @@ def open_update_window(openpype_version): return version_path +def show_message_dialog(title, message): + if os.getenv("OPENPYPE_HEADLESS_MODE"): + print("!!! Can't open dialog in headless mode. Exiting.") + sys.exit(1) + from Qt import QtWidgets, QtCore + from .message_dialog import MessageDialog + + scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None) + if scale_attr is not None: + QtWidgets.QApplication.setAttribute(scale_attr) + + app = QtWidgets.QApplication.instance() + if not app: + app = QtWidgets.QApplication(sys.argv) + + dialog = MessageDialog(title, message) + dialog.open() + + app.exec_() + + __all__ = [ "BootstrapRepos", "open_dialog", "open_update_window", + "show_message_dialog", "version" ] diff --git a/igniter/message_dialog.py b/igniter/message_dialog.py new file mode 100644 index 0000000000..88a086df1e --- /dev/null +++ b/igniter/message_dialog.py @@ -0,0 +1,44 @@ +from Qt import QtWidgets, QtGui + +from .tools import ( + load_stylesheet, + get_openpype_icon_path +) + + +class MessageDialog(QtWidgets.QDialog): + def __init__(self, title, message): + super(MessageDialog, self).__init__() + + # Set logo as icon of window + icon_path = get_openpype_icon_path() + pixmap_openpype_logo = QtGui.QPixmap(icon_path) + self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo)) + + # Set title + self.setWindowTitle(title) + + # Set message + label_widget = QtWidgets.QLabel(message, self) + + ok_btn = QtWidgets.QPushButton("OK", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn, 0) + + layout = QtWidgets.QVBoxLayout(self) + # layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(label_widget, 1) + layout.addLayout(btns_layout, 0) + + ok_btn.clicked.connect(self._on_ok_clicked) + + self._label_widget = label_widget + self._ok_btn = ok_btn + + def _on_ok_clicked(self): + self.close() + + def showEvent(self, event): + super(MessageDialog, self).showEvent(event) + self.setStyleSheet(load_stylesheet()) From 500339088d51a2c66536aaffd5bd07ea7a4f6619 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:24:25 +0100 Subject: [PATCH 035/105] added new exception into tools --- igniter/tools.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/igniter/tools.py b/igniter/tools.py index 72b98f1f82..2595140582 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -16,6 +16,11 @@ from pymongo.errors import ( ) +class OpenPypeVersionNotFound(Exception): + """OpenPype version was not found in remote and local repository.""" + pass + + def should_add_certificate_path_to_mongo_url(mongo_url): """Check if should add ca certificate to mongo url. From 21b9926be40079b04d970c690d11db8ffb421329 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:30:15 +0100 Subject: [PATCH 036/105] simplified _find_frozen_openpype --- start.py | 120 +++++++++++++++++++++++-------------------------------- 1 file changed, 49 insertions(+), 71 deletions(-) diff --git a/start.py b/start.py index 05b7da6308..91221da75d 100644 --- a/start.py +++ b/start.py @@ -196,7 +196,8 @@ from igniter import BootstrapRepos # noqa: E402 from igniter.tools import ( get_openpype_global_settings, get_openpype_path_from_db, - validate_mongo_connection + validate_mongo_connection, + OpenPypeVersionNotFound ) # noqa from igniter.bootstrap_repos import OpenPypeVersion # noqa: E402 @@ -645,67 +646,60 @@ def _find_frozen_openpype(use_version: str = None, (if requested). """ - version_path = None - openpype_version = None - openpype_versions = bootstrap.find_openpype(include_zips=True, - staging=use_staging) - # get local frozen version and add it to detected version so if it is - # newer it will be used instead. - local_version_str = bootstrap.get_version( - Path(os.environ["OPENPYPE_ROOT"])) + # Collect OpenPype versions + openpype_versions = bootstrap.find_openpype( + include_zips=True, + staging=use_staging + ) + openpype_root = Path(os.environ["OPENPYPE_ROOT"]) + local_version_str = bootstrap.get_version(openpype_root) + studio_version = OpenPypeVersion.get_expected_studio_version(use_staging) if local_version_str: local_version = OpenPypeVersion( version=local_version_str, - path=Path(os.environ["OPENPYPE_ROOT"])) + path=openpype_root + ) if local_version not in openpype_versions: openpype_versions.append(local_version) - openpype_versions.sort() - # if latest is currently running, ditch whole list - # and run from current without installing it. - if local_version == openpype_versions[-1]: - os.environ["OPENPYPE_TRYOUT"] = "1" - openpype_versions = [] + + # Find OpenPype version that should be used + openpype_version = None + if use_version is not None: + # Version was specified with arguments or env OPENPYPE_VERSION + # - should crash if version is not available + _print("Finding specified version \"{}\"".format(use_version)) + for version in openpype_versions: + if version == use_version: + openpype_version = version + break + + if openpype_version is None: + raise OpenPypeVersionNotFound( + "Requested version \"{}\" was not found.".format(use_version) + ) + + elif studio_version: + _print("Finding studio version \"{}\"".format(studio_version)) + # Look for version specified by settings + for version in openpype_versions: + if version == studio_version: + openpype_version = version + break + if openpype_version is None: + raise OpenPypeVersionNotFound(( + "Requested OpenPype version \"{}\" defined by settings" + " was not found." + ).format(use_version)) + else: - _print("!!! Warning: cannot determine current running version.") + # Use latest available version + _print("Finding latest version") + openpype_versions.sort() + openpype_version = openpype_versions[-1] - if not os.getenv("OPENPYPE_TRYOUT"): - try: - # use latest one found (last in the list is latest) - openpype_version = openpype_versions[-1] - except IndexError: - # no OpenPype version found, run Igniter and ask for them. - _print('*** No OpenPype versions found.') - if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": - _print("!!! Cannot open Igniter dialog in headless mode.") - sys.exit(1) - _print("--- launching setup UI ...") - import igniter - return_code = igniter.open_dialog() - if return_code == 2: - os.environ["OPENPYPE_TRYOUT"] = "1" - if return_code == 3: - # run OpenPype after installation - - _print('>>> Finding OpenPype again ...') - openpype_versions = bootstrap.find_openpype( - staging=use_staging) - try: - openpype_version = openpype_versions[-1] - except IndexError: - _print(("!!! Something is wrong and we didn't " - "found it again.")) - sys.exit(1) - elif return_code != 2: - _print(f" . finished ({return_code})") - sys.exit(return_code) - - if not openpype_versions: - # no openpype versions found anyway, lets use then the one - # shipped with frozen OpenPype - if not os.getenv("OPENPYPE_TRYOUT"): - _print("*** Still no luck finding OpenPype.") - _print(("*** We'll try to use the one coming " - "with OpenPype installation.")) + # get local frozen version and add it to detected version so if it is + # newer it will be used instead. + if local_version == openpype_version: version_path = _bootstrap_from_code(use_version, use_staging) openpype_version = OpenPypeVersion( version=BootstrapRepos.get_version(version_path), @@ -713,22 +707,6 @@ def _find_frozen_openpype(use_version: str = None, _initialize_environment(openpype_version) return version_path - # get path of version specified in `--use-version` - local_version = bootstrap.get_version(OPENPYPE_ROOT) - if use_version and use_version != local_version: - # force the one user has selected - openpype_version = None - 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: - openpype_version = sorted(found)[-1] - if not openpype_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) is_inside = False try: From 5d03abe627d890785e8657931559f8dac39d66b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:30:51 +0100 Subject: [PATCH 037/105] removed play animation part --- start.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/start.py b/start.py index 91221da75d..5f416a47b8 100644 --- a/start.py +++ b/start.py @@ -866,16 +866,6 @@ def boot(): # ------------------------------------------------------------------------ _startup_validations() - # ------------------------------------------------------------------------ - # Play animation - # ------------------------------------------------------------------------ - - # from igniter.terminal_splash import play_animation - - # don't play for silenced commands - # if all(item not in sys.argv for item in silent_commands): - # play_animation() - # ------------------------------------------------------------------------ # Process arguments # ------------------------------------------------------------------------ From 9b369de50c2ad5a281fb0ff84fcb7988f26c224f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:38:43 +0100 Subject: [PATCH 038/105] raise OpenPypeVersionNotFound in run from code too --- start.py | 57 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/start.py b/start.py index 5f416a47b8..69774c3a6c 100644 --- a/start.py +++ b/start.py @@ -769,45 +769,50 @@ def _bootstrap_from_code(use_version, use_staging): # get current version of OpenPype local_version = bootstrap.get_local_live_version() - 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 not openpype_versions: + raise OpenPypeVersionNotFound( + "Didn't find any staging versions." + ) + + # Use last found staging version + version_to_use = openpype_versions[-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 + os.environ["OPENPYPE_REPOS_ROOT"] = ( + version_path / "openpype" + ).as_posix() _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: - version_to_use = sorted(found)[-1] + version_to_use = None + for version in openpype_versions: + if str(version) == use_version: + version_to_use = version + break - if version_to_use: - # use specified - 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"] = use_version - 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() - else: - _print(f"!!! Requested version {use_version} was not found.") - list_versions(openpype_versions, local_version) - sys.exit(1) + if version_to_use is None: + raise OpenPypeVersionNotFound( + "Requested version \"{}\" was not found.".format(use_version) + ) + + # use specified + 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"] = use_version + version_path = version_to_use.path + os.environ["OPENPYPE_REPOS_ROOT"] = ( + version_path / "openpype" + ).as_posix() + _openpype_root = version_to_use.path.as_posix() else: os.environ["OPENPYPE_VERSION"] = local_version version_path = Path(_openpype_root) From a98bdc7d7f2e91ce6c3855d6ca0ed95c6e84be45 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:39:24 +0100 Subject: [PATCH 039/105] catch and handle OpenPypeVersioNotFound exception --- start.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/start.py b/start.py index 69774c3a6c..853e6d13a0 100644 --- a/start.py +++ b/start.py @@ -972,6 +972,15 @@ def boot(): # find versions of OpenPype to be used with frozen code try: version_path = _find_frozen_openpype(use_version, use_staging) + except OpenPypeVersionNotFound as exc: + message = str(exc) + _print(message) + if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": + list_versions(openpype_versions, local_version) + else: + igniter.show_message_dialog("Version not found", message) + sys.exit(1) + except RuntimeError as e: # no version to run _print(f"!!! {e}") @@ -984,7 +993,17 @@ def boot(): sys.exit(1) _print(f"--- version is valid") else: - version_path = _bootstrap_from_code(use_version, use_staging) + try: + version_path = _bootstrap_from_code(use_version, use_staging) + + except OpenPypeVersionNotFound as exc: + message = str(exc) + _print(message) + if os.environ.get("OPENPYPE_HEADLESS_MODE") == "1": + list_versions(openpype_versions, local_version) + else: + igniter.show_message_dialog("Version not found", message) + sys.exit(1) # 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 b0099ea5d4271d997f80932535cc2817574fc73a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Dec 2021 18:39:45 +0100 Subject: [PATCH 040/105] skipped already done imports --- start.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/start.py b/start.py index 853e6d13a0..32fa4bd062 100644 --- a/start.py +++ b/start.py @@ -521,7 +521,7 @@ def _process_arguments() -> tuple: if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": _print("!!! Cannot open Igniter dialog in headless mode.") sys.exit(1) - import igniter + return_code = igniter.open_dialog() # this is when we want to run OpenPype without installing anything. @@ -719,12 +719,12 @@ def _find_frozen_openpype(use_version: str = None, if not is_inside: # install latest version to user data dir - if os.getenv("OPENPYPE_HEADLESS_MODE", "0") != "1": - import igniter - version_path = igniter.open_update_window(openpype_version) - else: + if os.getenv("OPENPYPE_HEADLESS_MODE") == "1": version_path = bootstrap.install_version( - openpype_version, force=True) + openpype_version, force=True + ) + else: + version_path = igniter.open_update_window(openpype_version) openpype_version.path = version_path _initialize_environment(openpype_version) From 0dd3eca360fd10693f3c3a1de6375d55151d7c41 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 17:25:24 +0100 Subject: [PATCH 041/105] added helper method to change style properties --- openpype/tools/settings/settings/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index f8378ed18c..bc6c6d10ff 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -26,6 +26,14 @@ class BaseWidget(QtWidgets.QWidget): self.label_widget = None self.create_ui() + @staticmethod + def set_style_property(obj, property_name, property_value): + if obj.property(property_name) == property_value: + return + + obj.setProperty(property_name, property_value) + obj.style().polish(obj) + def scroll_to(self, widget): self.category_widget.scroll_to(widget) From 0479d544bd3c23539c3367ac2dada2d8345c7ad0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 17:25:41 +0100 Subject: [PATCH 042/105] update geo on show of completer view --- openpype/tools/settings/settings/widgets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 4b88c1f93f..bfe8339219 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -95,6 +95,10 @@ class CompleterView(QtWidgets.QListView): return result + def showEvent(self, event): + super(CompleterView, self).showEvent(event) + self._update_geo() + def _update_geo(self): size_hint = self.sizeHint() self.resize(size_hint.width(), size_hint.height()) From ccce04309bac2318a6ebf87916b0f646e3204dd1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 17:26:33 +0100 Subject: [PATCH 043/105] renamed exception BaseInvalidValueType to BaseInvalidValue --- openpype/settings/entities/__init__.py | 4 ++-- openpype/settings/entities/base_entity.py | 4 ++-- openpype/settings/entities/color_entity.py | 6 +++--- openpype/settings/entities/exceptions.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 4efd358297..a173e2454f 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -57,7 +57,7 @@ from .exceptions import ( SchemaError, DefaultsNotDefined, StudioDefaultsNotDefined, - BaseInvalidValueType, + BaseInvalidValue, InvalidValueType, InvalidKeySymbols, SchemaMissingFileInfo, @@ -130,7 +130,7 @@ from .op_version_entity import ( __all__ = ( "DefaultsNotDefined", "StudioDefaultsNotDefined", - "BaseInvalidValueType", + "BaseInvalidValue", "InvalidValueType", "InvalidKeySymbols", "SchemaMissingFileInfo", diff --git a/openpype/settings/entities/base_entity.py b/openpype/settings/entities/base_entity.py index 341968bd75..12754d345f 100644 --- a/openpype/settings/entities/base_entity.py +++ b/openpype/settings/entities/base_entity.py @@ -9,7 +9,7 @@ from .lib import ( ) from .exceptions import ( - BaseInvalidValueType, + BaseInvalidValue, InvalidValueType, SchemeGroupHierarchyBug, EntitySchemaError @@ -432,7 +432,7 @@ class BaseItemEntity(BaseEntity): try: new_value = self.convert_to_valid_type(value) - except BaseInvalidValueType: + except BaseInvalidValue: new_value = NOT_SET if new_value is not NOT_SET: diff --git a/openpype/settings/entities/color_entity.py b/openpype/settings/entities/color_entity.py index 3becf2d865..bdaab6f583 100644 --- a/openpype/settings/entities/color_entity.py +++ b/openpype/settings/entities/color_entity.py @@ -1,7 +1,7 @@ from .lib import STRING_TYPE from .input_entities import InputEntity from .exceptions import ( - BaseInvalidValueType, + BaseInvalidValue, InvalidValueType ) @@ -47,7 +47,7 @@ class ColorEntity(InputEntity): reason = "Color entity expect 4 items in list got {}".format( len(value) ) - raise BaseInvalidValueType(reason, self.path) + raise BaseInvalidValue(reason, self.path) new_value = [] for item in value: @@ -60,7 +60,7 @@ class ColorEntity(InputEntity): reason = ( "Color entity expect 4 integers in range 0-255 got {}" ).format(value) - raise BaseInvalidValueType(reason, self.path) + raise BaseInvalidValue(reason, self.path) new_value.append(item) # Make sure diff --git a/openpype/settings/entities/exceptions.py b/openpype/settings/entities/exceptions.py index f352c94f20..d1728a7b12 100644 --- a/openpype/settings/entities/exceptions.py +++ b/openpype/settings/entities/exceptions.py @@ -15,14 +15,14 @@ class StudioDefaultsNotDefined(Exception): super(StudioDefaultsNotDefined, self).__init__(msg) -class BaseInvalidValueType(Exception): +class BaseInvalidValue(Exception): def __init__(self, reason, path): msg = "Path \"{}\". {}".format(path, reason) self.msg = msg - super(BaseInvalidValueType, self).__init__(msg) + super(BaseInvalidValue, self).__init__(msg) -class InvalidValueType(BaseInvalidValueType): +class InvalidValueType(BaseInvalidValue): def __init__(self, valid_types, invalid_type, path): joined_types = ", ".join( [str(valid_type) for valid_type in valid_types] From 703368c5b0eaddc93172d6487ac1b5dcb0e5f91b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 17:27:02 +0100 Subject: [PATCH 044/105] added validation of version value with convert_to_valid_type --- .../settings/entities/op_version_entity.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 2458f03852..20495a0d42 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -2,10 +2,15 @@ from openpype.lib.openpype_version import ( op_version_control_available, get_remote_versions, openpype_path_is_set, - openpype_path_is_accessible + openpype_path_is_accessible, + get_OpenPypeVersion ) from .input_entities import TextEntity -from .lib import OverrideState +from .lib import ( + OverrideState, + NOT_SET +) +from .exceptions import BaseInvalidValue class OpenPypeVersionInput(TextEntity): @@ -32,6 +37,21 @@ class OpenPypeVersionInput(TextEntity): state, *args, **kwargs ) + def convert_to_valid_type(self, value): + if value and value is not NOT_SET: + OpenPypeVersion = get_OpenPypeVersion() + if OpenPypeVersion is not None: + try: + OpenPypeVersion(version=value) + except Exception: + raise BaseInvalidValue( + "Value \"{}\"is not valid version format.".format( + value + ), + self.path + ) + return super(OpenPypeVersionInput, self).convert_to_valid_type(value) + class ProductionVersionsInputEntity(OpenPypeVersionInput): schema_types = ["production-versions-text"] From 9acdf2e338ebd44c4321035b4e885e863c81deab Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 17:27:27 +0100 Subject: [PATCH 045/105] added style to info label --- openpype/style/data.json | 4 +++- openpype/style/style.css | 8 ++++++++ openpype/tools/settings/settings/item_widgets.py | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/style/data.json b/openpype/style/data.json index 026eaf4264..62573f015e 100644 --- a/openpype/style/data.json +++ b/openpype/style/data.json @@ -111,7 +111,9 @@ "focus-border": "#839caf", "image-btn": "#bfccd6", "image-btn-hover": "#189aea", - "image-btn-disabled": "#bfccd6" + "image-btn-disabled": "#bfccd6", + "version-exists": "#458056", + "version-not-found": "#ffc671" } } } diff --git a/openpype/style/style.css b/openpype/style/style.css index 0ef15a2511..9249db5f1e 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -1158,6 +1158,14 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { border-radius: 5px; } +#OpenPypeVersionLabel[state="success"] { + color: {color:settings:version-exists}; +} + +#OpenPypeVersionLabel[state="warning"] { + color: {color:settings:version-not-found}; +} + #ShadowWidget { font-size: 36pt; } diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 0c66162375..3a7a107965 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -424,6 +424,7 @@ class OpenPypeVersionText(TextWidget): def create_ui(self): super(OpenPypeVersionText, self).create_ui() info_widget = QtWidgets.QLabel("Latest", self) + info_widget.setObjectName("OpenPypeVersionLabel") self.content_layout.addWidget(info_widget, 1) self._info_widget = info_widget From 84022f9e0def952a44a7cbea3f35b690705c2c78 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 17:27:53 +0100 Subject: [PATCH 046/105] added more variants of info label with colors and ivalidation --- .../tools/settings/settings/item_widgets.py | 63 +++++++++++++++++-- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 3a7a107965..9f78060c87 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -4,6 +4,7 @@ from Qt import QtWidgets, QtCore, QtGui from openpype.widgets.sliders import NiceSlider from openpype.tools.settings import CHILD_OFFSET +from openpype.settings.entities.exceptions import BaseInvalidValue from .widgets import ( ExpandingWidget, @@ -423,7 +424,7 @@ class OpenPypeVersionText(TextWidget): def create_ui(self): super(OpenPypeVersionText, self).create_ui() - info_widget = QtWidgets.QLabel("Latest", self) + info_widget = QtWidgets.QLabel(self) info_widget.setObjectName("OpenPypeVersionLabel") self.content_layout.addWidget(info_widget, 1) @@ -431,17 +432,67 @@ class OpenPypeVersionText(TextWidget): def _update_info_widget(self): value = self.input_value() - if value == "": - self._info_widget.setText("Latest") + + message = "" + tooltip = "" + state = None + if self._is_invalid: + message = "Invalid version format" + + elif value == "": + message = "Use latest available version" + tooltip = "Latest version from OpenPype zip repository will used" + elif value in self.entity.value_hints: - self._info_widget.setText("Ok") + message = "Version {} will be used".format(value) + state = "success" + else: - self._info_widget.setText("Version not found from this workstation") + message = ( + "Version {} not found in listed versions".format(value) + ) + state = "warning" + if self.entity.value_hints: + tooltip = "Found versions are {}".format(", ".join( + ['"{}"'.format(hint) for hint in self.entity.value_hints] + )) + else: + tooltip = "No versions were found" + + self._info_widget.setText(message) + self._info_widget.setToolTip(tooltip) + self.set_style_property(self._info_widget, "state", state) + + def set_entity_value(self): + super(OpenPypeVersionText, self).set_entity_value() + self._invalidate() + self._update_info_widget() + + def _on_value_change_timer(self): + value = self.input_value() + self._invalidate() + if not self.is_invalid: + self.entity.set(value) + self.update_style() + else: + # Manually trigger hierachical style update + self.ignore_input_changes.set_ignore(True) + self.ignore_input_changes.set_ignore(False) + + self._update_info_widget() + + def _invalidate(self): + value = self.input_value() + try: + self.entity.convert_to_valid_type(value) + is_invalid = False + except BaseInvalidValue: + is_invalid = True + self._is_invalid = is_invalid def _on_entity_change(self): super(OpenPypeVersionText, self)._on_entity_change() self._refresh_completer() - self._update_info_widget() class NumberWidget(InputWidget): From 5181c52d12a2a7e3b2688d01ea7f05df73379d70 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 17:29:49 +0100 Subject: [PATCH 047/105] modified messages a little bit --- openpype/tools/settings/settings/item_widgets.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 9f78060c87..22f672da2b 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -437,27 +437,29 @@ class OpenPypeVersionText(TextWidget): tooltip = "" state = None if self._is_invalid: - message = "Invalid version format" + message = "Invalid OpenPype version format" elif value == "": message = "Use latest available version" - tooltip = "Latest version from OpenPype zip repository will used" + tooltip = ( + "Latest version from OpenPype zip repository will be used" + ) elif value in self.entity.value_hints: - message = "Version {} will be used".format(value) state = "success" + message = "Version {} will be used".format(value) else: + state = "warning" message = ( "Version {} not found in listed versions".format(value) ) - state = "warning" if self.entity.value_hints: - tooltip = "Found versions are {}".format(", ".join( + tooltip = "Listed versions: {}".format(", ".join( ['"{}"'.format(hint) for hint in self.entity.value_hints] )) else: - tooltip = "No versions were found" + tooltip = "No versions were listed" self._info_widget.setText(message) self._info_widget.setToolTip(tooltip) From 1588df3ca275165a3fb143ddcd5c9d4b4af92ea8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 17:37:03 +0100 Subject: [PATCH 048/105] added placeholder inheritance back --- openpype/tools/settings/settings/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 3e44bd3969..cc6e396db0 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -185,7 +185,7 @@ class CompleterView(QtWidgets.QListView): self._on_activated(index) -class SettingsLineEdit(QtWidgets.QLineEdit): +class SettingsLineEdit(PlaceholderLineEdit): focused_in = QtCore.Signal() def __init__(self, *args, **kwargs): From 767ef9d715c92f50493a1d20352e3df5ecaff63a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 18:31:14 +0100 Subject: [PATCH 049/105] sort versions earlier --- start.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/start.py b/start.py index 32fa4bd062..55008a9526 100644 --- a/start.py +++ b/start.py @@ -662,6 +662,8 @@ def _find_frozen_openpype(use_version: str = None, if local_version not in openpype_versions: openpype_versions.append(local_version) + openpype_versions.sort() + # Find OpenPype version that should be used openpype_version = None if use_version is not None: @@ -694,7 +696,6 @@ def _find_frozen_openpype(use_version: str = None, else: # Use latest available version _print("Finding latest version") - openpype_versions.sort() openpype_version = openpype_versions[-1] # get local frozen version and add it to detected version so if it is From ce204157273e74fd15abd9a37eafbd7722e533a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 18:31:40 +0100 Subject: [PATCH 050/105] added option to pass "latest" to use version argument --- start.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/start.py b/start.py index 55008a9526..28c04600c4 100644 --- a/start.py +++ b/start.py @@ -667,13 +667,17 @@ def _find_frozen_openpype(use_version: str = None, # Find OpenPype version that should be used openpype_version = None if use_version is not None: - # Version was specified with arguments or env OPENPYPE_VERSION - # - should crash if version is not available - _print("Finding specified version \"{}\"".format(use_version)) - for version in openpype_versions: - if version == use_version: - openpype_version = version - break + if use_version.lower() == "latest": + openpype_version = openpype_versions[-1] + else: + use_version_obj = OpenPypeVersion(use_version) + # Version was specified with arguments or env OPENPYPE_VERSION + # - should crash if version is not available + _print("Finding specified version \"{}\"".format(use_version)) + for version in openpype_versions: + if version == use_version_obj: + openpype_version = version + break if openpype_version is None: raise OpenPypeVersionNotFound( From 52c5424f2b59987876eb0412dfbdab2c8af0cd09 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 18:31:50 +0100 Subject: [PATCH 051/105] modified messages --- start.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/start.py b/start.py index 28c04600c4..6fa624b5cd 100644 --- a/start.py +++ b/start.py @@ -681,7 +681,9 @@ def _find_frozen_openpype(use_version: str = None, if openpype_version is None: raise OpenPypeVersionNotFound( - "Requested version \"{}\" was not found.".format(use_version) + "Requested version \"{}\" was not found.".format( + use_version + ) ) elif studio_version: @@ -695,7 +697,7 @@ def _find_frozen_openpype(use_version: str = None, raise OpenPypeVersionNotFound(( "Requested OpenPype version \"{}\" defined by settings" " was not found." - ).format(use_version)) + ).format(studio_version)) else: # Use latest available version From d2edf3da743459bf3b043d5c855098b04cb66e79 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 19:02:37 +0100 Subject: [PATCH 052/105] don't return latest version --- 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 9446b3e8ce..3db9dd0b91 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -589,7 +589,7 @@ class OpenPypeVersion(semver.VersionInfo): """ result = get_expected_studio_version_str(staging) if not result: - return cls.get_latest_version(staging, False) + return None return OpenPypeVersion(version=result) From 02b8e57a81a2b4c9782dc69a57e7b53c29598d8a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 19:03:16 +0100 Subject: [PATCH 053/105] return None from get_latest_version if there are any openpype versions available --- igniter/bootstrap_repos.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 3db9dd0b91..163db07f59 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -559,7 +559,8 @@ class OpenPypeVersion(semver.VersionInfo): staging: bool = False, local: bool = False) -> OpenPypeVersion: """Get latest available version. - This is utility version to get latest version from all found. + This is utility version to get latest version from all found except + build. Args: staging (bool, optional): List staging versions if True. @@ -572,6 +573,8 @@ class OpenPypeVersion(semver.VersionInfo): openpype_versions = OpenPypeVersion.get_available_versions( staging, local) + if not openpype_versions: + return None return openpype_versions[-1] @classmethod From a160e2229068007f28a320800c0d983784f11405 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 19:03:29 +0100 Subject: [PATCH 054/105] have ability to get build version --- igniter/bootstrap_repos.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 163db07f59..920eb5fa6b 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -554,6 +554,19 @@ class OpenPypeVersion(semver.VersionInfo): return sorted(_openpype_versions) + @staticmethod + def get_build_version(): + """Get version of OpenPype inside build.""" + openpype_root = Path(os.environ["OPENPYPE_ROOT"]) + build_version_str = BootstrapRepos.get_version(openpype_root) + build_version = None + if build_version_str: + build_version = OpenPypeVersion( + version=build_version_str, + path=openpype_root + ) + return build_version + @staticmethod def get_latest_version( staging: bool = False, local: bool = False) -> OpenPypeVersion: From 406ece7e44db78e44d365632ec6ac1ddaf8494fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 19:03:55 +0100 Subject: [PATCH 055/105] use new methods in start.py --- start.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/start.py b/start.py index 6fa624b5cd..80baff5496 100644 --- a/start.py +++ b/start.py @@ -651,16 +651,11 @@ def _find_frozen_openpype(use_version: str = None, include_zips=True, staging=use_staging ) - openpype_root = Path(os.environ["OPENPYPE_ROOT"]) - local_version_str = bootstrap.get_version(openpype_root) + local_version = OpenPypeVersion.get_build_version() + if local_version not in openpype_versions: + openpype_versions.append(local_version) + studio_version = OpenPypeVersion.get_expected_studio_version(use_staging) - if local_version_str: - local_version = OpenPypeVersion( - version=local_version_str, - path=openpype_root - ) - if local_version not in openpype_versions: - openpype_versions.append(local_version) openpype_versions.sort() @@ -668,6 +663,7 @@ def _find_frozen_openpype(use_version: str = None, openpype_version = None if use_version is not None: if use_version.lower() == "latest": + _print("Finding latest version") openpype_version = openpype_versions[-1] else: use_version_obj = OpenPypeVersion(use_version) From 715e1a6d32e19fe53ecd3c2061fac6cd87549f04 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 19:06:16 +0100 Subject: [PATCH 056/105] cache build version is it probably won't change during process time --- igniter/bootstrap_repos.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 920eb5fa6b..5e8efd1cc4 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -63,6 +63,7 @@ class OpenPypeVersion(semver.VersionInfo): staging = False path = None _VERSION_REGEX = re.compile(r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$") # noqa: E501 + _build_version = None def __init__(self, *args, **kwargs): """Create OpenPype version. @@ -554,18 +555,20 @@ class OpenPypeVersion(semver.VersionInfo): return sorted(_openpype_versions) - @staticmethod - def get_build_version(): + @classmethod + def get_build_version(cls): """Get version of OpenPype inside build.""" - openpype_root = Path(os.environ["OPENPYPE_ROOT"]) - build_version_str = BootstrapRepos.get_version(openpype_root) - build_version = None - if build_version_str: - build_version = OpenPypeVersion( - version=build_version_str, - path=openpype_root - ) - return build_version + if cls._build_version is None: + openpype_root = Path(os.environ["OPENPYPE_ROOT"]) + build_version_str = BootstrapRepos.get_version(openpype_root) + build_version = None + if build_version_str: + build_version = OpenPypeVersion( + version=build_version_str, + path=openpype_root + ) + cls._build_version = build_version + return cls._build_version @staticmethod def get_latest_version( From 02686ff096cb3dafd51aa74d1d288c6b7c556506 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 19:31:29 +0100 Subject: [PATCH 057/105] added get build version --- openpype/lib/openpype_version.py | 10 ++++++++++ openpype/settings/entities/op_version_entity.py | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 42ee454378..7d9dc6ef94 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -20,6 +20,16 @@ def op_version_control_available(): return True +def get_build_version(): + """Get OpenPype version inside build. + + This version is not returned by any other functions here. + """ + if op_version_control_available(): + return get_OpenPypeVersion().get_build_version() + return None + + def get_available_versions(*args, **kwargs): """Get list of available versions.""" if op_version_control_available(): diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 20495a0d42..cf2150f12e 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -3,7 +3,8 @@ from openpype.lib.openpype_version import ( get_remote_versions, openpype_path_is_set, openpype_path_is_accessible, - get_OpenPypeVersion + get_OpenPypeVersion, + get_build_version ) from .input_entities import TextEntity from .lib import ( @@ -57,11 +58,15 @@ class ProductionVersionsInputEntity(OpenPypeVersionInput): schema_types = ["production-versions-text"] def _get_openpype_versions(self): - return get_remote_versions(production=True) + versions = get_remote_versions(staging=False, production=True) + versions.append(get_build_version()) + return sorted(versions) class StagingVersionsInputEntity(OpenPypeVersionInput): schema_types = ["staging-versions-text"] def _get_openpype_versions(self): - return get_remote_versions(staging=True) + versions = get_remote_versions(staging=True, production=False) + versions.append(get_build_version()) + return sorted(versions) From b3c1bbd84ee31795a3425d5fd245c2d639b8b58f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 19:31:36 +0100 Subject: [PATCH 058/105] removed unused imports --- openpype/settings/entities/enum_entity.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 7c8721556f..fb6099e82a 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -1,10 +1,4 @@ import copy -from openpype.lib.openpype_version import ( - op_version_control_available, - get_remote_versions, - openpype_path_is_set, - openpype_path_is_accessible -) from .input_entities import InputEntity from .exceptions import EntitySchemaError from .lib import ( From c05ec98b6049202aacb3273ed2d5ea38b97b5e00 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 8 Dec 2021 19:32:07 +0100 Subject: [PATCH 059/105] removed current verions functions and replace then with get_expected_studio_version --- openpype/lib/openpype_version.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 7d9dc6ef94..9a92454eca 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -2,7 +2,11 @@ Access to logic from igniter is available only for OpenPype processes. Is meant to be able check OpenPype versions for studio. The logic is dependent -on igniter's logic of processing. +on igniter's inner logic of versions. + +Keep in mind that all functions except 'get_build_version' does not return +OpenPype version located in build but versions available in remote versions +repository or locally available. """ import sys @@ -40,42 +44,42 @@ def get_available_versions(*args, **kwargs): def openpype_path_is_set(): + """OpenPype repository path is set in settings.""" if op_version_control_available(): return get_OpenPypeVersion().openpype_path_is_set() return None def openpype_path_is_accessible(): + """OpenPype version repository path can be accessed.""" if op_version_control_available(): return get_OpenPypeVersion().openpype_path_is_accessible() return None def get_local_versions(*args, **kwargs): + """OpenPype versions available on this workstation.""" if op_version_control_available(): return get_OpenPypeVersion().get_local_versions(*args, **kwargs) return None def get_remote_versions(*args, **kwargs): + """OpenPype versions in repository path.""" if op_version_control_available(): return get_OpenPypeVersion().get_remote_versions(*args, **kwargs) return None def get_latest_version(*args, **kwargs): + """Get latest version from repository path.""" if op_version_control_available(): return get_OpenPypeVersion().get_latest_version(*args, **kwargs) return None -def get_current_production_version(): +def get_expected_studio_version(staging=False): + """Expected production or staging version in studio.""" if op_version_control_available(): - return get_OpenPypeVersion().get_production_version() - return None - - -def get_current_staging_version(): - if op_version_control_available(): - return get_OpenPypeVersion().get_staging_version() + return get_OpenPypeVersion().get_expected_studio_version(staging) return None From 1a60f7fa0f047132c79a9a148b4ca8d1a474977e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 11:33:32 +0100 Subject: [PATCH 060/105] keep previous find_openpype implementation --- igniter/bootstrap_repos.py | 62 ++++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 5e8efd1cc4..9e58f5bad2 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1115,21 +1115,65 @@ class BootstrapRepos: @staticmethod def find_openpype( + self, openpype_path: Union[Path, str] = None, staging: bool = False, include_zips: bool = False) -> Union[List[OpenPypeVersion], None]: + """Get ordered dict of detected OpenPype version. + Resolution order for OpenPype is following: + + 1) First we test for ``OPENPYPE_PATH`` environment variable + 2) We try to find ``openPypePath`` in registry setting + 3) We use user data directory + + Args: + openpype_path (Path or str, optional): Try to find OpenPype on + the given path or url. + staging (bool, optional): Filter only staging version, skip them + otherwise. + include_zips (bool, optional): If set True it will try to find + OpenPype in zip files in given directory. + + Returns: + dict of Path: Dictionary of detected OpenPype version. + Key is version, value is path to zip file. + + None: if OpenPype is not found. + + Todo: + implement git/url support as OpenPype location, so it would be + possible to enter git url, OpenPype would check it out and if it is + ok install it as normal version. + + """ + if openpype_path and not isinstance(openpype_path, Path): + raise NotImplementedError( + ("Finding OpenPype in non-filesystem locations is" + " not implemented yet.")) + + dir_to_search = self.data_dir + user_versions = self.get_openpype_versions(self.data_dir, staging) + # if we have openpype_path specified, search only there. if openpype_path: - openpype_versions = OpenPypeVersion.get_versions_from_directory( - openpype_path) - # filter out staging - - openpype_versions = [ - v for v in openpype_versions if v.is_staging() == staging - ] - + dir_to_search = openpype_path else: - openpype_versions = OpenPypeVersion.get_available_versions(staging) + if os.getenv("OPENPYPE_PATH"): + if Path(os.getenv("OPENPYPE_PATH")).exists(): + dir_to_search = Path(os.getenv("OPENPYPE_PATH")) + else: + try: + registry_dir = Path( + str(self.registry.get_item("openPypePath"))) + if registry_dir.exists(): + dir_to_search = registry_dir + + except ValueError: + # nothing found in registry, we'll use data dir + pass + + openpype_versions = self.get_openpype_versions(dir_to_search, staging) + openpype_versions += user_versions # remove zip file version if needed. if not include_zips: From 77730e8d2b3b2e13026e77a618271936e5a2a7b5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 11:35:03 +0100 Subject: [PATCH 061/105] added new methods to find openpype versions --- igniter/bootstrap_repos.py | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 9e58f5bad2..a258f71a20 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1114,6 +1114,64 @@ class BootstrapRepos: os.environ["PYTHONPATH"] = os.pathsep.join(paths) @staticmethod + def find_openpype_version(version, staging): + if isinstance(version, str): + version = OpenPypeVersion(version=version) + + build_version = OpenPypeVersion.get_build_version() + if build_version == version: + return build_version + + local_versions = OpenPypeVersion.get_local_versions( + staging=staging, production=not staging + ) + zip_version = None + for local_version in local_versions: + if local_version == version: + if local_version.suffix.lower() == ".zip": + zip_version = local_version + else: + return local_version + + if zip_version is not None: + return zip_version + + remote_versions = OpenPypeVersion.get_remote_versions( + staging=staging, production=not staging + ) + for remote_version in remote_versions: + if remote_version == version: + return remote_version + return None + + @staticmethod + def find_latest_openpype_version(staging): + build_version = OpenPypeVersion.get_build_version() + local_versions = OpenPypeVersion.get_local_versions( + staging=staging + ) + remote_versions = OpenPypeVersion.get_remote_versions( + staging=staging + ) + all_versions = local_versions + remote_versions + if not staging: + all_versions.append(build_version) + + if not all_versions: + return None + + all_versions.sort() + latest_version = all_versions[-1] + if latest_version == build_version: + return latest_version + + if not latest_version.path.is_dir(): + for version in local_versions: + if version == latest_version and version.path.is_dir(): + latest_version = version + break + return latest_version + def find_openpype( self, openpype_path: Union[Path, str] = None, From d22214415c17a8037353406c90a509954dcce8ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 11:59:57 +0100 Subject: [PATCH 062/105] simplified get build version --- igniter/bootstrap_repos.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index a258f71a20..7a367e6f09 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -561,13 +561,11 @@ class OpenPypeVersion(semver.VersionInfo): if cls._build_version is None: openpype_root = Path(os.environ["OPENPYPE_ROOT"]) build_version_str = BootstrapRepos.get_version(openpype_root) - build_version = None if build_version_str: - build_version = OpenPypeVersion( + cls._build_version = OpenPypeVersion( version=build_version_str, path=openpype_root ) - cls._build_version = build_version return cls._build_version @staticmethod From bc301fc7893551767763545e689d6ade875da428 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:02:19 +0100 Subject: [PATCH 063/105] modified get latest versions --- igniter/bootstrap_repos.py | 56 ++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 7a367e6f09..7de4c5db4b 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -570,26 +570,58 @@ class OpenPypeVersion(semver.VersionInfo): @staticmethod def get_latest_version( - staging: bool = False, local: bool = False) -> OpenPypeVersion: + staging: bool = False, + local: bool = None, + remote: bool = None + ) -> OpenPypeVersion: """Get latest available version. - This is utility version to get latest version from all found except - build. + The version does not contain information about path and source. + + This is utility version to get latest version from all found. Build + version is not listed if staging is enabled. + + Arguments 'local' and 'remote' define if local and remote repository + versions are used. All versions are used if both are not set (or set + to 'None'). If only one of them is set to 'True' the other is disabled. + It is possible to set both to 'True' (same as both set to None) and to + 'False' in that case only build version can be used. Args: staging (bool, optional): List staging versions if True. - local (bool, optional): List only local versions. - - See also: - OpenPypeVersion.get_available_versions() - + local (bool, optional): List local versions if True. + remote (bool, optional): List remote versions if True. """ - openpype_versions = OpenPypeVersion.get_available_versions( - staging, local) + if local is None and remote is None: + local = True + remote = True - if not openpype_versions: + elif local is None and not remote: + local = True + + elif remote is None and not local: + remote = True + + build_version = OpenPypeVersion.get_build_version() + local_versions = [] + remote_versions = [] + if local: + local_versions = OpenPypeVersion.get_local_versions( + staging=staging + ) + if remote: + remote_versions = OpenPypeVersion.get_remote_versions( + staging=staging + ) + all_versions = local_versions + remote_versions + if not staging: + all_versions.append(build_version) + + if not all_versions: return None - return openpype_versions[-1] + + all_versions.sort() + return all_versions[-1] @classmethod def get_expected_studio_version(cls, staging=False): From 105e6241b46da79102d50fb84c7376155478a31a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:02:32 +0100 Subject: [PATCH 064/105] fixed zip check --- 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 7de4c5db4b..2f49fee064 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -1158,7 +1158,7 @@ class BootstrapRepos: zip_version = None for local_version in local_versions: if local_version == version: - if local_version.suffix.lower() == ".zip": + if local_version.path.suffix.lower() == ".zip": zip_version = local_version else: return local_version From 3e04f294e09d98102a673ebc5182258a07866070 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:02:44 +0100 Subject: [PATCH 065/105] removed unused method --- igniter/bootstrap_repos.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 2f49fee064..77fcd15f1e 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -477,39 +477,6 @@ class OpenPypeVersion(semver.VersionInfo): filtered_versions.append(version) return list(sorted(set(filtered_versions))) - @classmethod - def get_available_versions( - cls, staging: bool = False, local: bool = False) -> List: - """Get ordered dict of detected OpenPype version. - - Resolution order for OpenPype is following: - - 1) First we test for ``OPENPYPE_PATH`` environment variable - 2) We try to find ``openPypePath`` in registry setting - 3) We use user data directory - - Only versions from 3) will be listed when ``local`` is set to True. - - Args: - staging (bool, optional): List staging versions if True. - local (bool, optional): List only local versions. - - """ - user_versions = cls.get_local_versions() - # if we have openpype_path specified, search only there. - openpype_versions = [] - if not local: - openpype_versions = cls.get_remote_versions() - openpype_versions += user_versions - - # remove duplicates and staging/production - openpype_versions = [ - v for v in openpype_versions if v.is_staging() == staging - ] - openpype_versions = sorted(list(set(openpype_versions))) - - return openpype_versions - @staticmethod def get_versions_from_directory(openpype_dir: Path) -> List: """Get all detected OpenPype versions in directory. From d71b8e2e40d2215a7b7c644d2debbf6549e7e171 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:03:23 +0100 Subject: [PATCH 066/105] modified use version argument handling to be able pass "latest" --- start.py | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/start.py b/start.py index 80baff5496..29afb759c5 100644 --- a/start.py +++ b/start.py @@ -467,23 +467,41 @@ def _process_arguments() -> tuple: use_version = None use_staging = False commands = [] - for arg in sys.argv: - if arg == "--use-version": - _print("!!! Please use option --use-version like:") - _print(" --use-version=3.0.0") - 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 + # OpenPype version specification through arguments + use_version_arg = "--use-version" + + for arg in sys.argv: + if arg.startswith(use_version_arg): + # Remove arg from sys argv + sys.argv.remove(arg) + # Extract string after use version arg + use_version_value = arg[len(use_version_arg):] + + if ( + not use_version_value + or not use_version_value.startswith("=") + ): + _print("!!! Please use option --use-version like:") + _print(" --use-version=3.0.0") + sys.exit(1) + + version_str = use_version_value[1:] + use_version = None + if version_str.lower() == "latest": + use_version = "latest" else: + m = re.search( + r"(?P\d+\.\d+\.\d+(?:\S*)?)", version_str + ) + if m and m.group('version'): + use_version = m.group('version') + _print(">>> Requested version [ {} ]".format(use_version)) + if "+staging" in use_version: + use_staging = True + break + + if use_version is None: _print("!!! Requested version isn't in correct format.") _print((" Use --list-versions to find out" " proper version string.")) From 27df5b5d924b18c0fcd8c0ae321a3464070e37c7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:03:50 +0100 Subject: [PATCH 067/105] use new methods in bootstrap from code --- start.py | 66 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/start.py b/start.py index 29afb759c5..674cc66a0f 100644 --- a/start.py +++ b/start.py @@ -781,6 +781,13 @@ def _bootstrap_from_code(use_version, use_staging): # run through repos and add them to `sys.path` and `PYTHONPATH` # set root _openpype_root = OPENPYPE_ROOT + # Unset use version if latest should be used + # - when executed from code then code is expected as latest + # - when executed from build then build is already marked as latest + # in '_find_frozen_openpype' + if use_version and use_version.lower() == "latest": + use_version = None + if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(_openpype_root)) switch_str = f" - will switch to {use_version}" if use_version else "" @@ -790,43 +797,35 @@ def _bootstrap_from_code(use_version, use_staging): # get current version of OpenPype local_version = bootstrap.get_local_live_version() - openpype_versions = bootstrap.find_openpype( - include_zips=True, staging=use_staging) - if use_staging and not use_version: - if not openpype_versions: - raise OpenPypeVersionNotFound( - "Didn't find any staging versions." + # All cases when should be used different version than build + if (use_version and use_version != local_version) or use_staging: + if use_version: + # Explicit version should be used + version_to_use = bootstrap.find_openpype_version( + use_version, use_staging ) - - # Use last found staging version - version_to_use = openpype_versions[-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() - _openpype_root = version_to_use.path.as_posix() - - elif use_version and use_version != local_version: - version_to_use = None - for version in openpype_versions: - if str(version) == use_version: - version_to_use = version - break - - if version_to_use is None: - raise OpenPypeVersionNotFound( - "Requested version \"{}\" was not found.".format(use_version) + if version_to_use is None: + raise OpenPypeVersionNotFound( + "Requested version \"{}\" was not found.".format( + use_version + ) + ) + else: + # Staging version should be used + version_to_use = bootstrap.find_latest_openpype_version( + use_staging ) + if version_to_use is None: + if use_staging: + reason = "Didn't find any staging versions." + else: + # This reason is backup for possible bug in code + reason = "Didn't find any versions." + raise OpenPypeVersionNotFound(reason) - # use specified + # Start extraction of version if needed if version_to_use.path.is_file(): - version_to_use.path = bootstrap.extract_openpype( - version_to_use) + version_to_use.path = bootstrap.extract_openpype(version_to_use) bootstrap.add_paths_from_directory(version_to_use.path) os.environ["OPENPYPE_VERSION"] = use_version version_path = version_to_use.path @@ -834,6 +833,7 @@ def _bootstrap_from_code(use_version, use_staging): version_path / "openpype" ).as_posix() _openpype_root = version_to_use.path.as_posix() + else: os.environ["OPENPYPE_VERSION"] = local_version version_path = Path(_openpype_root) From d219ff0cd18219051a00de65ccfa9c6990d94e5b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:04:13 +0100 Subject: [PATCH 068/105] use new methods in find frozen openpype --- start.py | 59 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/start.py b/start.py index 674cc66a0f..58c88df340 100644 --- a/start.py +++ b/start.py @@ -665,33 +665,25 @@ def _find_frozen_openpype(use_version: str = None, """ # Collect OpenPype versions - openpype_versions = bootstrap.find_openpype( - include_zips=True, - staging=use_staging - ) - local_version = OpenPypeVersion.get_build_version() - if local_version not in openpype_versions: - openpype_versions.append(local_version) - + build_version = OpenPypeVersion.get_build_version() + # Expected version that should be used by studio settings + # - this option is used only if version is not explictly set and if + # studio has set explicit version in settings studio_version = OpenPypeVersion.get_expected_studio_version(use_staging) - openpype_versions.sort() - - # Find OpenPype version that should be used - openpype_version = None if use_version is not None: + # Specific version is defined if use_version.lower() == "latest": - _print("Finding latest version") - openpype_version = openpype_versions[-1] + # Version says to use latest version + _print("Finding latest version defined by use version") + openpype_version = bootstrap.find_latest_openpype_version( + use_staging + ) else: - use_version_obj = OpenPypeVersion(use_version) - # Version was specified with arguments or env OPENPYPE_VERSION - # - should crash if version is not available _print("Finding specified version \"{}\"".format(use_version)) - for version in openpype_versions: - if version == use_version_obj: - openpype_version = version - break + openpype_version = bootstrap.find_openpype_version( + use_version, use_staging + ) if openpype_version is None: raise OpenPypeVersionNotFound( @@ -700,13 +692,12 @@ def _find_frozen_openpype(use_version: str = None, ) ) - elif studio_version: + elif studio_version is not None: + # Studio has defined a version to use _print("Finding studio version \"{}\"".format(studio_version)) - # Look for version specified by settings - for version in openpype_versions: - if version == studio_version: - openpype_version = version - break + openpype_version = bootstrap.find_openpype_version( + studio_version, use_staging + ) if openpype_version is None: raise OpenPypeVersionNotFound(( "Requested OpenPype version \"{}\" defined by settings" @@ -714,13 +705,21 @@ def _find_frozen_openpype(use_version: str = None, ).format(studio_version)) else: - # Use latest available version + # Default behavior to use latest version _print("Finding latest version") - openpype_version = openpype_versions[-1] + openpype_version = bootstrap.find_latest_openpype_version( + use_staging + ) + if openpype_version is None: + if use_staging: + reason = "Didn't find any staging versions." + else: + reason = "Didn't find any versions." + raise OpenPypeVersionNotFound(reason) # get local frozen version and add it to detected version so if it is # newer it will be used instead. - if local_version == openpype_version: + if build_version == openpype_version: version_path = _bootstrap_from_code(use_version, use_staging) openpype_version = OpenPypeVersion( version=BootstrapRepos.get_version(version_path), From a82bf00d7a28242aa0bc134b4220a806cd108b00 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:13:00 +0100 Subject: [PATCH 069/105] removed unused imports --- openpype/settings/entities/op_version_entity.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index cf2150f12e..97b3efa028 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -1,8 +1,5 @@ from openpype.lib.openpype_version import ( - op_version_control_available, get_remote_versions, - openpype_path_is_set, - openpype_path_is_accessible, get_OpenPypeVersion, get_build_version ) From 070078d1578cb3bb177cbb53a807bd145602d084 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:13:12 +0100 Subject: [PATCH 070/105] do not add build version to staging --- openpype/settings/entities/op_version_entity.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 97b3efa028..bd481dc497 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -65,5 +65,4 @@ class StagingVersionsInputEntity(OpenPypeVersionInput): def _get_openpype_versions(self): versions = get_remote_versions(staging=True, production=False) - versions.append(get_build_version()) return sorted(versions) From 43c552dbbfd74712eeebb57e22bc1ef4b807b92e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:13:36 +0100 Subject: [PATCH 071/105] added option to pass global settings to 'get_expected_studio_version_str' --- igniter/bootstrap_repos.py | 7 +++++-- igniter/tools.py | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 77fcd15f1e..70e6a75b9d 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -591,19 +591,22 @@ class OpenPypeVersion(semver.VersionInfo): return all_versions[-1] @classmethod - def get_expected_studio_version(cls, staging=False): + def get_expected_studio_version(cls, staging=False, global_settings=None): """Expected OpenPype version that should be used at the moment. If version is not defined in settings the latest found version is used. + Using precached global settings is needed for usage inside OpenPype. + Args: staging (bool): Staging version or production version. + global_settings (dict): Optional precached global settings. Returns: OpenPypeVersion: Version that should be used. """ - result = get_expected_studio_version_str(staging) + result = get_expected_studio_version_str(staging, global_settings) if not result: return None return OpenPypeVersion(version=result) diff --git a/igniter/tools.py b/igniter/tools.py index 2595140582..735402e9a2 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -187,17 +187,21 @@ def get_openpype_path_from_db(url: str) -> Union[str, None]: return None -def get_expected_studio_version_str(staging=False) -> str: +def get_expected_studio_version_str( + staging=False, global_settings=None +) -> str: """Version that should be currently used in studio. Args: staging (bool): Get current version for staging. + global_settings (dict): Optional precached global settings. Returns: str: OpenPype version which should be used. Empty string means latest. """ mongo_url = os.environ.get("OPENPYPE_MONGO") - global_settings = get_openpype_global_settings(mongo_url) + if global_settings is None: + global_settings = get_openpype_global_settings(mongo_url) if staging: key = "staging_version" else: From ad99d2a841f4cf581f13f2ef1e9f93e95c84760e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Dec 2021 12:21:12 +0100 Subject: [PATCH 072/105] added few docstrings --- igniter/__init__.py | 4 ++++ igniter/message_dialog.py | 2 +- .../settings/entities/op_version_entity.py | 19 ++++++++++++++++++- openpype/tools/settings/settings/base.py | 1 + 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/igniter/__init__.py b/igniter/__init__.py index 6f06c1eb90..02cba6a483 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -12,6 +12,9 @@ from .bootstrap_repos import ( ) from .version import __version__ as version +# Store OpenPypeVersion to 'sys.modules' +# - this makes it available in OpenPype processes without modifying +# 'sys.path' or 'PYTHONPATH' if "OpenPypeVersion" not in sys.modules: sys.modules["OpenPypeVersion"] = OpenPypeVersion @@ -64,6 +67,7 @@ def open_update_window(openpype_version): def show_message_dialog(title, message): + """Show dialog with a message and title to user.""" if os.getenv("OPENPYPE_HEADLESS_MODE"): print("!!! Can't open dialog in headless mode. Exiting.") sys.exit(1) diff --git a/igniter/message_dialog.py b/igniter/message_dialog.py index 88a086df1e..c8e875cc37 100644 --- a/igniter/message_dialog.py +++ b/igniter/message_dialog.py @@ -7,6 +7,7 @@ from .tools import ( class MessageDialog(QtWidgets.QDialog): + """Simple message dialog with title, message and OK button.""" def __init__(self, title, message): super(MessageDialog, self).__init__() @@ -27,7 +28,6 @@ class MessageDialog(QtWidgets.QDialog): btns_layout.addWidget(ok_btn, 0) layout = QtWidgets.QVBoxLayout(self) - # layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(label_widget, 1) layout.addLayout(btns_layout, 0) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index bd481dc497..62c5bf4ff9 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -12,6 +12,16 @@ from .exceptions import BaseInvalidValue class OpenPypeVersionInput(TextEntity): + """Entity to store OpenPype version to use. + + It is text input as creating of settings on different machines may + affect which versions are available so it must have option to set OpenPype + version which is not available for machine where settings entities are + loaded. + + It is possible to enter empty string. In that case is used any latest + version. Any other string must match regex of OpenPype version semantic. + """ def _item_initialization(self): super(OpenPypeVersionInput, self)._item_initialization() self.multiline = False @@ -19,9 +29,13 @@ class OpenPypeVersionInput(TextEntity): self.value_hints = [] def _get_openpype_versions(self): - return [] + """This is abstract method returning version hints for UI purposes.""" + raise NotImplementedError(( + "{} does not have implemented '_get_openpype_versions'" + ).format(self.__class__.__name__)) def set_override_state(self, state, *args, **kwargs): + """Update value hints for UI purposes.""" value_hints = [] if state is OverrideState.STUDIO: versions = self._get_openpype_versions() @@ -36,6 +50,7 @@ class OpenPypeVersionInput(TextEntity): ) def convert_to_valid_type(self, value): + """Add validation of version regex.""" if value and value is not NOT_SET: OpenPypeVersion = get_OpenPypeVersion() if OpenPypeVersion is not None: @@ -52,6 +67,7 @@ class OpenPypeVersionInput(TextEntity): class ProductionVersionsInputEntity(OpenPypeVersionInput): + """Entity meant only for global settings to define production version.""" schema_types = ["production-versions-text"] def _get_openpype_versions(self): @@ -61,6 +77,7 @@ class ProductionVersionsInputEntity(OpenPypeVersionInput): class StagingVersionsInputEntity(OpenPypeVersionInput): + """Entity meant only for global settings to define staging version.""" schema_types = ["staging-versions-text"] def _get_openpype_versions(self): diff --git a/openpype/tools/settings/settings/base.py b/openpype/tools/settings/settings/base.py index bc6c6d10ff..48c2b42ebd 100644 --- a/openpype/tools/settings/settings/base.py +++ b/openpype/tools/settings/settings/base.py @@ -28,6 +28,7 @@ class BaseWidget(QtWidgets.QWidget): @staticmethod def set_style_property(obj, property_name, property_value): + """Change QWidget property and polish it's style.""" if obj.property(property_name) == property_value: return From 6b5706ef78a3c3bde0a4efd2235bd46dfe8b9d81 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Dec 2021 10:38:23 +0100 Subject: [PATCH 073/105] added method to get installed version --- igniter/bootstrap_repos.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 70e6a75b9d..b603fe74f1 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -522,6 +522,16 @@ class OpenPypeVersion(semver.VersionInfo): return sorted(_openpype_versions) + @staticmethod + def get_installed_version_str() -> str: + """Get version of local OpenPype.""" + + version = {} + path = Path(os.environ["OPENPYPE_ROOT"]) / "openpype" / "version.py" + with open(path, "r") as fp: + exec(fp.read(), version) + return version["__version__"] + @classmethod def get_build_version(cls): """Get version of OpenPype inside build.""" From 1b4e5dd4435ae7acba7be6f21f6304a99ed9e938 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Dec 2021 10:40:20 +0100 Subject: [PATCH 074/105] replaced get_local_live_version with get_installed_version_str --- igniter/bootstrap_repos.py | 12 +----------- start.py | 6 +++--- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index b603fe74f1..890df453a1 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -690,16 +690,6 @@ class BootstrapRepos: return v.path return None - @staticmethod - def get_local_live_version() -> str: - """Get version of local OpenPype.""" - - version = {} - path = Path(os.environ["OPENPYPE_ROOT"]) / "openpype" / "version.py" - with open(path, "r") as fp: - exec(fp.read(), version) - return version["__version__"] - @staticmethod def get_version(repo_dir: Path) -> Union[str, None]: """Get version of OpenPype in given directory. @@ -747,7 +737,7 @@ class BootstrapRepos: # version and use it as a source. Otherwise repo_dir is user # entered location. if not repo_dir: - version = self.get_local_live_version() + version = OpenPypeVersion.get_installed_version_str() repo_dir = self.live_repo_dir else: version = self.get_version(repo_dir) diff --git a/start.py b/start.py index 58c88df340..975249b4e2 100644 --- a/start.py +++ b/start.py @@ -794,7 +794,7 @@ def _bootstrap_from_code(use_version, use_staging): assert local_version else: # get current version of OpenPype - local_version = bootstrap.get_local_live_version() + local_version = OpenPypeVersion.get_installed_version_str() # All cases when should be used different version than build if (use_version and use_version != local_version) or use_staging: @@ -930,7 +930,7 @@ def boot(): if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(OPENPYPE_ROOT)) else: - local_version = bootstrap.get_local_live_version() + local_version = OpenPypeVersion.get_installed_version_str() if "validate" in commands: _print(f">>> Validating version [ {use_version} ]") @@ -978,7 +978,7 @@ def boot(): if getattr(sys, 'frozen', False): local_version = bootstrap.get_version(Path(_openpype_root)) else: - local_version = bootstrap.get_local_live_version() + local_version = OpenPypeVersion.get_installed_version_str() list_versions(openpype_versions, local_version) sys.exit(1) From 53e362f3db616ee95585856780b557cdcc502a25 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 10 Dec 2021 10:40:42 +0100 Subject: [PATCH 075/105] renamed build verion to installed version --- igniter/bootstrap_repos.py | 35 +++++++++++++++++------------------ start.py | 4 ++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py index 890df453a1..db62cbbe91 100644 --- a/igniter/bootstrap_repos.py +++ b/igniter/bootstrap_repos.py @@ -63,7 +63,7 @@ class OpenPypeVersion(semver.VersionInfo): staging = False path = None _VERSION_REGEX = re.compile(r"(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$") # noqa: E501 - _build_version = None + _installed_version = None def __init__(self, *args, **kwargs): """Create OpenPype version. @@ -533,17 +533,16 @@ class OpenPypeVersion(semver.VersionInfo): return version["__version__"] @classmethod - def get_build_version(cls): + def get_installed_version(cls): """Get version of OpenPype inside build.""" - if cls._build_version is None: - openpype_root = Path(os.environ["OPENPYPE_ROOT"]) - build_version_str = BootstrapRepos.get_version(openpype_root) - if build_version_str: - cls._build_version = OpenPypeVersion( - version=build_version_str, - path=openpype_root + if cls._installed_version is None: + installed_version_str = cls.get_installed_version_str() + if installed_version_str: + cls._installed_version = OpenPypeVersion( + version=installed_version_str, + path=Path(os.environ["OPENPYPE_ROOT"]) ) - return cls._build_version + return cls._installed_version @staticmethod def get_latest_version( @@ -579,7 +578,7 @@ class OpenPypeVersion(semver.VersionInfo): elif remote is None and not local: remote = True - build_version = OpenPypeVersion.get_build_version() + installed_version = OpenPypeVersion.get_installed_version() local_versions = [] remote_versions = [] if local: @@ -592,7 +591,7 @@ class OpenPypeVersion(semver.VersionInfo): ) all_versions = local_versions + remote_versions if not staging: - all_versions.append(build_version) + all_versions.append(installed_version) if not all_versions: return None @@ -1118,9 +1117,9 @@ class BootstrapRepos: if isinstance(version, str): version = OpenPypeVersion(version=version) - build_version = OpenPypeVersion.get_build_version() - if build_version == version: - return build_version + installed_version = OpenPypeVersion.get_installed_version() + if installed_version == version: + return installed_version local_versions = OpenPypeVersion.get_local_versions( staging=staging, production=not staging @@ -1146,7 +1145,7 @@ class BootstrapRepos: @staticmethod def find_latest_openpype_version(staging): - build_version = OpenPypeVersion.get_build_version() + installed_version = OpenPypeVersion.get_installed_version() local_versions = OpenPypeVersion.get_local_versions( staging=staging ) @@ -1155,14 +1154,14 @@ class BootstrapRepos: ) all_versions = local_versions + remote_versions if not staging: - all_versions.append(build_version) + all_versions.append(installed_version) if not all_versions: return None all_versions.sort() latest_version = all_versions[-1] - if latest_version == build_version: + if latest_version == installed_version: return latest_version if not latest_version.path.is_dir(): diff --git a/start.py b/start.py index 975249b4e2..5a5039cd5c 100644 --- a/start.py +++ b/start.py @@ -665,7 +665,7 @@ def _find_frozen_openpype(use_version: str = None, """ # Collect OpenPype versions - build_version = OpenPypeVersion.get_build_version() + installed_version = OpenPypeVersion.get_installed_version() # Expected version that should be used by studio settings # - this option is used only if version is not explictly set and if # studio has set explicit version in settings @@ -719,7 +719,7 @@ def _find_frozen_openpype(use_version: str = None, # get local frozen version and add it to detected version so if it is # newer it will be used instead. - if build_version == openpype_version: + if installed_version == openpype_version: version_path = _bootstrap_from_code(use_version, use_staging) openpype_version = OpenPypeVersion( version=BootstrapRepos.get_version(version_path), From 3f4510a9c902b7f8d299ee5f73b2ebac8a9a232d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 17 Dec 2021 13:11:21 +0100 Subject: [PATCH 076/105] renamed 'get_build_version' to 'get_installed_version' in openpype --- openpype/lib/openpype_version.py | 6 +++--- openpype/settings/entities/op_version_entity.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/lib/openpype_version.py b/openpype/lib/openpype_version.py index 9a92454eca..e3a4e1fa3e 100644 --- a/openpype/lib/openpype_version.py +++ b/openpype/lib/openpype_version.py @@ -4,7 +4,7 @@ Access to logic from igniter is available only for OpenPype processes. Is meant to be able check OpenPype versions for studio. The logic is dependent on igniter's inner logic of versions. -Keep in mind that all functions except 'get_build_version' does not return +Keep in mind that all functions except 'get_installed_version' does not return OpenPype version located in build but versions available in remote versions repository or locally available. """ @@ -24,13 +24,13 @@ def op_version_control_available(): return True -def get_build_version(): +def get_installed_version(): """Get OpenPype version inside build. This version is not returned by any other functions here. """ if op_version_control_available(): - return get_OpenPypeVersion().get_build_version() + return get_OpenPypeVersion().get_installed_version() return None diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 62c5bf4ff9..6f6243cfee 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -1,7 +1,7 @@ from openpype.lib.openpype_version import ( get_remote_versions, get_OpenPypeVersion, - get_build_version + get_installed_version ) from .input_entities import TextEntity from .lib import ( @@ -72,7 +72,7 @@ class ProductionVersionsInputEntity(OpenPypeVersionInput): def _get_openpype_versions(self): versions = get_remote_versions(staging=False, production=True) - versions.append(get_build_version()) + versions.append(get_installed_version()) return sorted(versions) From 3a95b99e42bf3903bbd5e66e6cb2558a5e880353 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Dec 2021 17:56:50 +0100 Subject: [PATCH 077/105] Re-use polyConstraint from openpype.host.maya.api.lib --- .../plugins/publish/validate_mesh_ngons.py | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py b/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py index e8cc019b52..839aab0d0b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py +++ b/openpype/hosts/maya/plugins/publish/validate_mesh_ngons.py @@ -7,21 +7,6 @@ from avalon import maya from openpype.hosts.maya.api import lib -def polyConstraint(objects, *args, **kwargs): - kwargs.pop('mode', None) - - with lib.no_undo(flush=False): - with maya.maintained_selection(): - with lib.reset_polySelectConstraint(): - cmds.select(objects, r=1, noExpand=True) - # Acting as 'polyCleanupArgList' for n-sided polygon selection - cmds.polySelectConstraint(*args, mode=3, **kwargs) - result = cmds.ls(selection=True) - cmds.select(clear=True) - - return result - - class ValidateMeshNgons(pyblish.api.Validator): """Ensure that meshes don't have ngons @@ -41,8 +26,17 @@ class ValidateMeshNgons(pyblish.api.Validator): @staticmethod def get_invalid(instance): - meshes = cmds.ls(instance, type='mesh') - return polyConstraint(meshes, type=8, size=3) + meshes = cmds.ls(instance, type='mesh', long=True) + + # Get all faces + faces = ['{0}.f[*]'.format(node) for node in meshes] + + # Filter to n-sided polygon faces (ngons) + invalid = lib.polyConstraint(faces, + t=0x0008, # type=face + size=3) # size=nsided + + return invalid def process(self, instance): """Process all the nodes in the instance "objectSet""" From 676e68f56b2bef3dcc479cdb69175d552da15116 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 3 Jan 2022 12:41:34 +0100 Subject: [PATCH 078/105] make sure anatomy does not return unicode strings --- openpype/lib/anatomy.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py index 66ecbd66d1..5f7285fe6c 100644 --- a/openpype/lib/anatomy.py +++ b/openpype/lib/anatomy.py @@ -1568,8 +1568,11 @@ class Roots: key_items = [self.env_prefix] for _key in keys: key_items.append(_key.upper()) + key = "_".join(key_items) - return {key: roots.value} + # Make sure key and value does not contain unicode + # - can happen in Python 2 hosts + return {str(key): str(roots.value)} output = {} for _key, _value in roots.items(): From cd0ad34595286cd06fd48c970713f4beba0d8a86 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 4 Jan 2022 16:34:10 +0100 Subject: [PATCH 079/105] modify comment Co-authored-by: Petr Kalis --- openpype/settings/entities/op_version_entity.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 6f6243cfee..55f8450edf 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -14,10 +14,8 @@ from .exceptions import BaseInvalidValue class OpenPypeVersionInput(TextEntity): """Entity to store OpenPype version to use. - It is text input as creating of settings on different machines may - affect which versions are available so it must have option to set OpenPype - version which is not available for machine where settings entities are - loaded. + Settings created on another machine may affect available versions on current user's machine. + Text input element is provided to explicitly set version not yet showing up the user's machine. It is possible to enter empty string. In that case is used any latest version. Any other string must match regex of OpenPype version semantic. From 18104b6aed36ee490992e76fe11b0b02b8d7ef1f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 4 Jan 2022 16:54:48 +0100 Subject: [PATCH 080/105] fix docstring text length --- openpype/settings/entities/op_version_entity.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 55f8450edf..1576173654 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -14,8 +14,9 @@ from .exceptions import BaseInvalidValue class OpenPypeVersionInput(TextEntity): """Entity to store OpenPype version to use. - Settings created on another machine may affect available versions on current user's machine. - Text input element is provided to explicitly set version not yet showing up the user's machine. + Settings created on another machine may affect available versions + on current user's machine. Text input element is provided to explicitly + set version not yet showing up the user's machine. It is possible to enter empty string. In that case is used any latest version. Any other string must match regex of OpenPype version semantic. From 07e151981c34e8cdb5710ce8a853be08530f0cb9 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 4 Jan 2022 17:14:00 +0100 Subject: [PATCH 081/105] skip duplicated versions --- openpype/settings/entities/op_version_entity.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 1576173654..6184f7640a 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -38,9 +38,10 @@ class OpenPypeVersionInput(TextEntity): value_hints = [] if state is OverrideState.STUDIO: versions = self._get_openpype_versions() - if versions is not None: - for version in versions: - value_hints.append(str(version)) + for version in versions: + version_str = str(version) + if version_str not in value_hints: + value_hints.append(version_str) self.value_hints = value_hints From 6b8b7f22e65aa964f0f9276672adcbabae8c392e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 5 Jan 2022 10:30:31 +0100 Subject: [PATCH 082/105] Fix #2473 Update "Repair Context" label to "Repair" --- openpype/action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/action.py b/openpype/action.py index 3fc6dd1a8f..50741875e4 100644 --- a/openpype/action.py +++ b/openpype/action.py @@ -72,7 +72,7 @@ class RepairContextAction(pyblish.api.Action): is available on the plugin. """ - label = "Repair Context" + label = "Repair" on = "failed" # This action is only available on a failed plug-in def process(self, context, plugin): From 65053ef948fc712ae939fc2756a0dc0abfed0643 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 15:33:15 +0100 Subject: [PATCH 083/105] trigger on task changed in set context method --- openpype/tools/workfiles/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/workfiles/app.py b/openpype/tools/workfiles/app.py index 0615ec0aca..f4a86050cb 100644 --- a/openpype/tools/workfiles/app.py +++ b/openpype/tools/workfiles/app.py @@ -1074,6 +1074,7 @@ class Window(QtWidgets.QMainWindow): if "task" in context: self.tasks_widget.select_task_name(context["task"]) + self._on_task_changed() def _on_asset_changed(self): asset_id = self.assets_widget.get_selected_asset_id() From d84d334ac6140a197c10ae88d6057d31bd8122e3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 16:15:22 +0100 Subject: [PATCH 084/105] fix style and dialog modality to parent widget --- openpype/tools/loader/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index ea45fd4364..f5ade04ae9 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -535,7 +535,7 @@ class SubsetWidget(QtWidgets.QWidget): self.load_ended.emit() if error_info: - box = LoadErrorMessageBox(error_info) + box = LoadErrorMessageBox(error_info, self) box.show() def selected_subsets(self, _groups=False, _merged=False, _other=True): @@ -1431,7 +1431,7 @@ class RepresentationWidget(QtWidgets.QWidget): self.load_ended.emit() if errors: - box = LoadErrorMessageBox(errors) + box = LoadErrorMessageBox(errors, self) box.show() def _get_optional_labels(self, loaders, selected_side): From 9d39d05e6bed847055a02a7a5f8686dcfa056545 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 17:44:16 +0100 Subject: [PATCH 085/105] styl have function to return path to images in resources --- openpype/style/__init__.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py index cb0595d522..ea88b342ee 100644 --- a/openpype/style/__init__.py +++ b/openpype/style/__init__.py @@ -1,8 +1,10 @@ import os import json import collections -from openpype import resources import six + +from openpype import resources + from .color_defs import parse_color @@ -12,6 +14,18 @@ _FONT_IDS = None current_dir = os.path.dirname(os.path.abspath(__file__)) +def get_style_image_path(image_name): + # All filenames are lowered + image_name = image_name.lower() + # Male sure filename has png extension + if not image_name.endswith(".png"): + image_name += ".png" + filepath = os.path.join(current_dir, "images", image_name) + if os.path.exists(filepath): + return filepath + return None + + def _get_colors_raw_data(): """Read data file with stylesheet fill values. @@ -160,6 +174,11 @@ def load_stylesheet(): return _STYLESHEET_CACHE -def app_icon_path(): +def get_app_icon_path(): """Path to OpenPype icon.""" return resources.get_openpype_icon_filepath() + + +def app_icon_path(): + # Backwards compatibility + return get_app_icon_path() From 13aa752c40fdfd1f61412eaea455930101b64045 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 17:47:32 +0100 Subject: [PATCH 086/105] added clickable frame to utils --- openpype/tools/utils/__init__.py | 4 ++++ openpype/tools/utils/widgets.py | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 7f15e64767..2a6d62453b 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -1,8 +1,12 @@ from .widgets import ( PlaceholderLineEdit, + BaseClickableFrame, + ClickableFrame, ) __all__ = ( "PlaceholderLineEdit", + "BaseClickableFrame", + "ClickableFrame", ) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 3bfa092a21..3e45600e33 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -25,6 +25,41 @@ class PlaceholderLineEdit(QtWidgets.QLineEdit): self.setPalette(filter_palette) +class BaseClickableFrame(QtWidgets.QFrame): + """Widget that catch left mouse click and can trigger a callback. + + Callback is defined by overriding `_mouse_release_callback`. + """ + def __init__(self, parent): + super(BaseClickableFrame, self).__init__(parent) + + self._mouse_pressed = False + + def _mouse_release_callback(self): + pass + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.LeftButton: + self._mouse_pressed = True + super(BaseClickableFrame, self).mousePressEvent(event) + + def mouseReleaseEvent(self, event): + if self._mouse_pressed: + self._mouse_pressed = False + if self.rect().contains(event.pos()): + self._mouse_release_callback() + + super(BaseClickableFrame, self).mouseReleaseEvent(event) + + +class ClickableFrame(BaseClickableFrame): + """Extended clickable frame which triggers 'clicked' signal.""" + clicked = QtCore.Signal() + + def _mouse_release_callback(self): + self.clicked.emit() + + class ImageButton(QtWidgets.QPushButton): """PushButton with icon and size of font. From b2f09495c49fda626a30ef6c1fd21d8918b9ea45 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 17:47:54 +0100 Subject: [PATCH 087/105] implemented expand button showing icon and having clicked signal --- openpype/tools/utils/__init__.py | 2 + openpype/tools/utils/widgets.py | 64 +++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 2a6d62453b..294b919b5c 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -2,6 +2,7 @@ from .widgets import ( PlaceholderLineEdit, BaseClickableFrame, ClickableFrame, + ExpandBtn, ) @@ -9,4 +10,5 @@ __all__ = ( "PlaceholderLineEdit", "BaseClickableFrame", "ClickableFrame", + "ExpandBtn" ) diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index 3e45600e33..c32eae043e 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -3,7 +3,10 @@ import logging from Qt import QtWidgets, QtCore, QtGui from avalon.vendor import qtawesome, qargparse -from openpype.style import get_objected_colors +from openpype.style import ( + get_objected_colors, + get_style_image_path +) log = logging.getLogger(__name__) @@ -60,6 +63,65 @@ class ClickableFrame(BaseClickableFrame): self.clicked.emit() +class ExpandBtnLabel(QtWidgets.QLabel): + """Label showing expand icon meant for ExpandBtn.""" + def __init__(self, parent): + super(ExpandBtnLabel, self).__init__(parent) + self._source_collapsed_pix = QtGui.QPixmap( + get_style_image_path("branch_closed") + ) + self._source_expanded_pix = QtGui.QPixmap( + get_style_image_path("branch_open") + ) + + self._current_image = self._source_collapsed_pix + self._collapsed = True + + def set_collapsed(self, collapsed): + if self._collapsed == collapsed: + return + self._collapsed = collapsed + if collapsed: + self._current_image = self._source_collapsed_pix + else: + self._current_image = self._source_expanded_pix + self._set_resized_pix() + + def resizeEvent(self, event): + self._set_resized_pix() + super(ExpandBtnLabel, self).resizeEvent(event) + + def _set_resized_pix(self): + size = int(self.fontMetrics().height() / 2) + if size < 1: + size = 1 + size += size % 2 + self.setPixmap( + self._current_image.scaled( + size, + size, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + ) + + +class ExpandBtn(ClickableFrame): + def __init__(self, parent=None): + super(ExpandBtn, self).__init__(parent) + + pixmap_label = ExpandBtnLabel(self) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(pixmap_label) + + self._pixmap_label = pixmap_label + + def set_collapsed(self, collapsed): + self._pixmap_label.set_collapsed(collapsed) + + class ImageButton(QtWidgets.QPushButton): """PushButton with icon and size of font. From 8ef9f44854bdd1e2b6001148eb4714d41dc9fee6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 17:49:11 +0100 Subject: [PATCH 088/105] implemented traceback widget showin traceback in loader --- openpype/tools/loader/widgets.py | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index f5ade04ae9..c8e371ee6f 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -18,6 +18,8 @@ from openpype.tools.utils.delegates import ( ) from openpype.tools.utils.widgets import ( OptionalMenu, + ClickableFrame, + ExpandBtn, PlaceholderLineEdit ) from openpype.tools.utils.views import ( @@ -64,6 +66,55 @@ class OverlayFrame(QtWidgets.QFrame): self.label_widget.setText(label) +class TracebackWidget(QtWidgets.QWidget): + def __init__(self, tb_text, parent): + super(TracebackWidget, self).__init__(parent) + + # Modify text to match html + # - add more replacements when needed + tb_text = ( + tb_text + .replace("<", "<") + .replace(">", ">") + .replace("\n", "
") + .replace(" ", " ") + ) + + expand_btn = ExpandBtn(self) + + clickable_frame = ClickableFrame(self) + clickable_layout = QtWidgets.QHBoxLayout(clickable_frame) + clickable_layout.setContentsMargins(0, 0, 0, 0) + + expand_label = QtWidgets.QLabel("Details", clickable_frame) + clickable_layout.addWidget(expand_label, 0) + clickable_layout.addStretch(1) + + show_details_layout = QtWidgets.QHBoxLayout() + show_details_layout.addWidget(expand_btn, 0) + show_details_layout.addWidget(clickable_frame, 1) + + text_widget = QtWidgets.QLabel(self) + text_widget.setText(tb_text) + text_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + text_widget.setVisible(False) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(show_details_layout, 0) + layout.addWidget(text_widget, 1) + + clickable_frame.clicked.connect(self._on_show_details_click) + expand_btn.clicked.connect(self._on_show_details_click) + + self._expand_btn = expand_btn + self._text_widget = text_widget + + def _on_show_details_click(self): + self._text_widget.setVisible(not self._text_widget.isVisible()) + self._expand_btn.set_collapsed(not self._text_widget.isVisible()) + + class LoadErrorMessageBox(QtWidgets.QDialog): def __init__(self, messages, parent=None): super(LoadErrorMessageBox, self).__init__(parent) From 72503a16e70c57b8ada5404a88f513afaa2f9161 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 17:50:14 +0100 Subject: [PATCH 089/105] reorganized contetn of error dialog --- openpype/tools/loader/widgets.py | 62 ++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index c8e371ee6f..80c4bade08 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -121,13 +121,10 @@ class LoadErrorMessageBox(QtWidgets.QDialog): self.setWindowTitle("Loading failed") self.setFocusPolicy(QtCore.Qt.StrongFocus) - body_layout = QtWidgets.QVBoxLayout(self) - main_label = ( "Failed to load items" ) main_label_widget = QtWidgets.QLabel(main_label, self) - body_layout.addWidget(main_label_widget) item_name_template = ( "Subset: {}
" @@ -136,38 +133,57 @@ class LoadErrorMessageBox(QtWidgets.QDialog): ) exc_msg_template = "{}" - for exc_msg, tb, repre, subset, version in messages: + content_scroll = QtWidgets.QScrollArea(self) + content_scroll.setWidgetResizable(True) + + content_widget = QtWidgets.QWidget(content_scroll) + content_scroll.setWidget(content_widget) + + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + for exc_msg, tb_text, repre, subset, version in messages: line = self._create_line() - body_layout.addWidget(line) + content_layout.addWidget(line) item_name = item_name_template.format(subset, version, repre) item_name_widget = QtWidgets.QLabel( item_name.replace("\n", "
"), self ) - body_layout.addWidget(item_name_widget) + item_name_widget.setWordWrap(True) + content_layout.addWidget(item_name_widget) exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
")) message_label_widget = QtWidgets.QLabel(exc_msg, self) - body_layout.addWidget(message_label_widget) + message_label_widget.setWordWrap(True) + content_layout.addWidget(message_label_widget) - if tb: - tb_widget = QtWidgets.QLabel(tb.replace("\n", "
"), self) - tb_widget.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - body_layout.addWidget(tb_widget) + if tb_text: + line = self._create_line() + tb_widget = TracebackWidget(tb_text, self) + content_layout.addWidget(line) + content_layout.addWidget(tb_widget) - footer_widget = QtWidgets.QWidget(self) - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - buttonBox = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical) - buttonBox.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Ok - ) - buttonBox.accepted.connect(self._on_accept) - footer_layout.addWidget(buttonBox, alignment=QtCore.Qt.AlignRight) - body_layout.addWidget(footer_widget) + content_layout.addStretch(1) - def _on_accept(self): + ok_btn = QtWidgets.QPushButton("OK", self) + + footer_layout = QtWidgets.QHBoxLayout() + footer_layout.addStretch(1) + footer_layout.addWidget(ok_btn, 0) + + bottom_line = self._create_line() + body_layout = QtWidgets.QVBoxLayout(self) + body_layout.addWidget(main_label_widget, 0) + body_layout.addWidget(content_scroll, 1) + body_layout.addWidget(bottom_line, 0) + body_layout.addLayout(footer_layout, 0) + + ok_btn.clicked.connect(self._on_ok_clicked) + + self.resize(660, 350) + + def _on_ok_clicked(self): self.close() def _create_line(self): From 631ba5b12e35f669113b55dcd8d2a45136f42088 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 17:50:50 +0100 Subject: [PATCH 090/105] line has different color --- openpype/tools/loader/widgets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 80c4bade08..0e392aef86 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -188,9 +188,9 @@ class LoadErrorMessageBox(QtWidgets.QDialog): def _create_line(self): line = QtWidgets.QFrame(self) - line.setFixedHeight(2) - line.setFrameShape(QtWidgets.QFrame.HLine) - line.setFrameShadow(QtWidgets.QFrame.Sunken) + line.setObjectName("Separator") + line.setMinimumHeight(2) + line.setMaximumHeight(2) return line From 049d0d27f9152c3b690a6f5546ec02547bb89faf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 17:51:00 +0100 Subject: [PATCH 091/105] added ability to copy report --- openpype/tools/loader/widgets.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 0e392aef86..a39ac7213a 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -142,7 +142,22 @@ class LoadErrorMessageBox(QtWidgets.QDialog): content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) + report_data = [] for exc_msg, tb_text, repre, subset, version in messages: + report_message = ( + "During load error happened on Subset: \"{subset}\"" + " Representation: \"{repre}\" Version: {version}" + "\n\nError message: {message}" + ).format( + subset=subset, + repre=repre, + version=version, + message=exc_msg + ) + if tb_text: + report_message += "\n\n{}".format(tb_text) + report_data.append(report_message) + line = self._create_line() content_layout.addWidget(line) @@ -166,9 +181,11 @@ class LoadErrorMessageBox(QtWidgets.QDialog): content_layout.addStretch(1) + copy_report_btn = QtWidgets.QPushButton("Copy report", self) ok_btn = QtWidgets.QPushButton("OK", self) footer_layout = QtWidgets.QHBoxLayout() + footer_layout.addWidget(copy_report_btn, 0) footer_layout.addStretch(1) footer_layout.addWidget(ok_btn, 0) @@ -179,13 +196,25 @@ class LoadErrorMessageBox(QtWidgets.QDialog): body_layout.addWidget(bottom_line, 0) body_layout.addLayout(footer_layout, 0) + copy_report_btn.clicked.connect(self._on_copy_report) ok_btn.clicked.connect(self._on_ok_clicked) self.resize(660, 350) + self._report_data = report_data + def _on_ok_clicked(self): self.close() + def _on_copy_report(self): + report_text = (10 * "*").join(self._report_data) + + mime_data = QtCore.QMimeData() + mime_data.setText(report_text) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) + def _create_line(self): line = QtWidgets.QFrame(self) line.setObjectName("Separator") From 7d72481b865fcdb78e8cd26c924f37a997533423 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 18:31:08 +0100 Subject: [PATCH 092/105] create base of error dialog in utils --- openpype/tools/loader/widgets.py | 159 ++++++--------------------- openpype/tools/utils/__init__.py | 6 +- openpype/tools/utils/error_dialog.py | 143 ++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 128 deletions(-) create mode 100644 openpype/tools/utils/error_dialog.py diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index a39ac7213a..3accaed5ab 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -11,15 +11,16 @@ from Qt import QtWidgets, QtCore, QtGui from avalon import api, pipeline from avalon.lib import HeroVersionType -from openpype.tools.utils import lib as tools_lib +from openpype.tools.utils import ( + ErrorMessageBox, + lib as tools_lib +) from openpype.tools.utils.delegates import ( VersionDelegate, PrettyTimeDelegate ) from openpype.tools.utils.widgets import ( OptionalMenu, - ClickableFrame, - ExpandBtn, PlaceholderLineEdit ) from openpype.tools.utils.views import ( @@ -66,66 +67,12 @@ class OverlayFrame(QtWidgets.QFrame): self.label_widget.setText(label) -class TracebackWidget(QtWidgets.QWidget): - def __init__(self, tb_text, parent): - super(TracebackWidget, self).__init__(parent) - - # Modify text to match html - # - add more replacements when needed - tb_text = ( - tb_text - .replace("<", "<") - .replace(">", ">") - .replace("\n", "
") - .replace(" ", " ") - ) - - expand_btn = ExpandBtn(self) - - clickable_frame = ClickableFrame(self) - clickable_layout = QtWidgets.QHBoxLayout(clickable_frame) - clickable_layout.setContentsMargins(0, 0, 0, 0) - - expand_label = QtWidgets.QLabel("Details", clickable_frame) - clickable_layout.addWidget(expand_label, 0) - clickable_layout.addStretch(1) - - show_details_layout = QtWidgets.QHBoxLayout() - show_details_layout.addWidget(expand_btn, 0) - show_details_layout.addWidget(clickable_frame, 1) - - text_widget = QtWidgets.QLabel(self) - text_widget.setText(tb_text) - text_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - text_widget.setVisible(False) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(show_details_layout, 0) - layout.addWidget(text_widget, 1) - - clickable_frame.clicked.connect(self._on_show_details_click) - expand_btn.clicked.connect(self._on_show_details_click) - - self._expand_btn = expand_btn - self._text_widget = text_widget - - def _on_show_details_click(self): - self._text_widget.setVisible(not self._text_widget.isVisible()) - self._expand_btn.set_collapsed(not self._text_widget.isVisible()) - - -class LoadErrorMessageBox(QtWidgets.QDialog): +class LoadErrorMessageBox(ErrorMessageBox): def __init__(self, messages, parent=None): - super(LoadErrorMessageBox, self).__init__(parent) - self.setWindowTitle("Loading failed") - self.setFocusPolicy(QtCore.Qt.StrongFocus) - - main_label = ( - "Failed to load items" - ) - main_label_widget = QtWidgets.QLabel(main_label, self) + self._messages = messages + super(LoadErrorMessageBox, self).__init__("Loading failed", parent) + def _create_content(self, content_layout): item_name_template = ( "Subset: {}
" "Version: {}
" @@ -133,31 +80,7 @@ class LoadErrorMessageBox(QtWidgets.QDialog): ) exc_msg_template = "{}" - content_scroll = QtWidgets.QScrollArea(self) - content_scroll.setWidgetResizable(True) - - content_widget = QtWidgets.QWidget(content_scroll) - content_scroll.setWidget(content_widget) - - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 0, 0) - - report_data = [] - for exc_msg, tb_text, repre, subset, version in messages: - report_message = ( - "During load error happened on Subset: \"{subset}\"" - " Representation: \"{repre}\" Version: {version}" - "\n\nError message: {message}" - ).format( - subset=subset, - repre=repre, - version=version, - message=exc_msg - ) - if tb_text: - report_message += "\n\n{}".format(tb_text) - report_data.append(report_message) - + for exc_msg, tb_text, repre, subset, version in self._messages: line = self._create_line() content_layout.addWidget(line) @@ -175,52 +98,34 @@ class LoadErrorMessageBox(QtWidgets.QDialog): if tb_text: line = self._create_line() - tb_widget = TracebackWidget(tb_text, self) + tb_widget = self._create_traceback_widget(tb_text, self) content_layout.addWidget(line) content_layout.addWidget(tb_widget) - content_layout.addStretch(1) + def _get_report_data(self): + report_data = [] + for exc_msg, tb_text, repre, subset, version in self._messages: + report_message = ( + "During load error happened on Subset: \"{subset}\"" + " Representation: \"{repre}\" Version: {version}" + "\n\nError message: {message}" + ).format( + subset=subset, + repre=repre, + version=version, + message=exc_msg + ) + if tb_text: + report_message += "\n\n{}".format(tb_text) + report_data.append(report_message) + return report_data - copy_report_btn = QtWidgets.QPushButton("Copy report", self) - ok_btn = QtWidgets.QPushButton("OK", self) - - footer_layout = QtWidgets.QHBoxLayout() - footer_layout.addWidget(copy_report_btn, 0) - footer_layout.addStretch(1) - footer_layout.addWidget(ok_btn, 0) - - bottom_line = self._create_line() - body_layout = QtWidgets.QVBoxLayout(self) - body_layout.addWidget(main_label_widget, 0) - body_layout.addWidget(content_scroll, 1) - body_layout.addWidget(bottom_line, 0) - body_layout.addLayout(footer_layout, 0) - - copy_report_btn.clicked.connect(self._on_copy_report) - ok_btn.clicked.connect(self._on_ok_clicked) - - self.resize(660, 350) - - self._report_data = report_data - - def _on_ok_clicked(self): - self.close() - - def _on_copy_report(self): - report_text = (10 * "*").join(self._report_data) - - mime_data = QtCore.QMimeData() - mime_data.setText(report_text) - QtWidgets.QApplication.instance().clipboard().setMimeData( - mime_data + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( + "Failed to load items" ) - - def _create_line(self): - line = QtWidgets.QFrame(self) - line.setObjectName("Separator") - line.setMinimumHeight(2) - line.setMaximumHeight(2) - return line + return label_widget class SubsetWidget(QtWidgets.QWidget): diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index 294b919b5c..4dd6bdd05f 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -5,10 +5,14 @@ from .widgets import ( ExpandBtn, ) +from .error_dialog import ErrorMessageBox + __all__ = ( "PlaceholderLineEdit", "BaseClickableFrame", "ClickableFrame", - "ExpandBtn" + "ExpandBtn", + + "ErrorMessageBox" ) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py new file mode 100644 index 0000000000..2f39ccf139 --- /dev/null +++ b/openpype/tools/utils/error_dialog.py @@ -0,0 +1,143 @@ +from Qt import QtWidgets, QtCore + +from .widgets import ClickableFrame, ExpandBtn + + +class TracebackWidget(QtWidgets.QWidget): + def __init__(self, tb_text, parent): + super(TracebackWidget, self).__init__(parent) + + # Modify text to match html + # - add more replacements when needed + tb_text = ( + tb_text + .replace("<", "<") + .replace(">", ">") + .replace("\n", "
") + .replace(" ", " ") + ) + + expand_btn = ExpandBtn(self) + + clickable_frame = ClickableFrame(self) + clickable_layout = QtWidgets.QHBoxLayout(clickable_frame) + clickable_layout.setContentsMargins(0, 0, 0, 0) + + expand_label = QtWidgets.QLabel("Details", clickable_frame) + clickable_layout.addWidget(expand_label, 0) + clickable_layout.addStretch(1) + + show_details_layout = QtWidgets.QHBoxLayout() + show_details_layout.addWidget(expand_btn, 0) + show_details_layout.addWidget(clickable_frame, 1) + + text_widget = QtWidgets.QLabel(self) + text_widget.setText(tb_text) + text_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + text_widget.setVisible(False) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(show_details_layout, 0) + layout.addWidget(text_widget, 1) + + clickable_frame.clicked.connect(self._on_show_details_click) + expand_btn.clicked.connect(self._on_show_details_click) + + self._expand_btn = expand_btn + self._text_widget = text_widget + + def _on_show_details_click(self): + self._text_widget.setVisible(not self._text_widget.isVisible()) + self._expand_btn.set_collapsed(not self._text_widget.isVisible()) + + +class ErrorMessageBox(QtWidgets.QDialog): + _default_width = 660 + _default_height = 350 + + def __init__(self, title, parent): + super(ErrorMessageBox, self).__init__(parent) + self.setWindowTitle(title) + self.setFocusPolicy(QtCore.Qt.StrongFocus) + + top_widget = self._create_top_widget(self) + + content_scroll = QtWidgets.QScrollArea(self) + content_scroll.setWidgetResizable(True) + + content_widget = QtWidgets.QWidget(content_scroll) + content_scroll.setWidget(content_widget) + + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + + self._create_content(content_layout) + + content_layout.addStretch(1) + + copy_report_btn = QtWidgets.QPushButton("Copy report", self) + ok_btn = QtWidgets.QPushButton("OK", self) + + footer_layout = QtWidgets.QHBoxLayout() + footer_layout.addWidget(copy_report_btn, 0) + footer_layout.addStretch(1) + footer_layout.addWidget(ok_btn, 0) + + bottom_line = self._create_line() + body_layout = QtWidgets.QVBoxLayout(self) + body_layout.addWidget(top_widget, 0) + body_layout.addWidget(content_scroll, 1) + body_layout.addWidget(bottom_line, 0) + body_layout.addLayout(footer_layout, 0) + + copy_report_btn.clicked.connect(self._on_copy_report) + ok_btn.clicked.connect(self._on_ok_clicked) + + self.resize(self._default_width, self._default_height) + + report_data = self._get_report_data() + if not report_data: + copy_report_btn.setVisible(False) + + self._report_data = report_data + self._content_widget = content_widget + + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( + "Something went wrong" + ) + return label_widget + + def _create_content(self, content_layout): + raise NotImplementedError( + "Method '_fill_content_layout' is not implemented!" + ) + + def _get_report_data(self): + return [] + + def _on_ok_clicked(self): + self.close() + + def _on_copy_report(self): + report_text = (10 * "*").join(self._report_data) + + mime_data = QtCore.QMimeData() + mime_data.setText(report_text) + QtWidgets.QApplication.instance().clipboard().setMimeData( + mime_data + ) + + def _create_line(self): + line = QtWidgets.QFrame(self) + line.setObjectName("Separator") + line.setMinimumHeight(2) + line.setMaximumHeight(2) + return line + + def _create_traceback_widget(self, traceback_text, parent=None): + if parent is None: + parent = self._content_widget + return TracebackWidget(traceback_text, parent) From 7107e797f0f0dfb0b5106062bcc3060cf3424de7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 18:42:18 +0100 Subject: [PATCH 093/105] extracted function for preparation of text with html symbols --- openpype/tools/loader/widgets.py | 50 ++++++++++++++-------------- openpype/tools/utils/error_dialog.py | 23 ++++++++----- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 3accaed5ab..ed130f765c 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -72,6 +72,31 @@ class LoadErrorMessageBox(ErrorMessageBox): self._messages = messages super(LoadErrorMessageBox, self).__init__("Loading failed", parent) + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( + "Failed to load items" + ) + return label_widget + + def _get_report_data(self): + report_data = [] + for exc_msg, tb_text, repre, subset, version in self._messages: + report_message = ( + "During load error happened on Subset: \"{subset}\"" + " Representation: \"{repre}\" Version: {version}" + "\n\nError message: {message}" + ).format( + subset=subset, + repre=repre, + version=version, + message=exc_msg + ) + if tb_text: + report_message += "\n\n{}".format(tb_text) + report_data.append(report_message) + return report_data + def _create_content(self, content_layout): item_name_template = ( "Subset: {}
" @@ -102,31 +127,6 @@ class LoadErrorMessageBox(ErrorMessageBox): content_layout.addWidget(line) content_layout.addWidget(tb_widget) - def _get_report_data(self): - report_data = [] - for exc_msg, tb_text, repre, subset, version in self._messages: - report_message = ( - "During load error happened on Subset: \"{subset}\"" - " Representation: \"{repre}\" Version: {version}" - "\n\nError message: {message}" - ).format( - subset=subset, - repre=repre, - version=version, - message=exc_msg - ) - if tb_text: - report_message += "\n\n{}".format(tb_text) - report_data.append(report_message) - return report_data - - def _create_top_widget(self, parent_widget): - label_widget = QtWidgets.QLabel(parent_widget) - label_widget.setText( - "Failed to load items" - ) - return label_widget - class SubsetWidget(QtWidgets.QWidget): """A widget that lists the published subsets for an asset""" diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index 2f39ccf139..0336f4bb08 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -3,20 +3,23 @@ from Qt import QtWidgets, QtCore from .widgets import ClickableFrame, ExpandBtn +def convert_text_for_html(text): + return ( + text + .replace("<", "<") + .replace(">", ">") + .replace("\n", "
") + .replace(" ", " ") + ) + + class TracebackWidget(QtWidgets.QWidget): def __init__(self, tb_text, parent): super(TracebackWidget, self).__init__(parent) # Modify text to match html # - add more replacements when needed - tb_text = ( - tb_text - .replace("<", "<") - .replace(">", ">") - .replace("\n", "
") - .replace(" ", " ") - ) - + tb_text = convert_text_for_html(tb_text) expand_btn = ExpandBtn(self) clickable_frame = ClickableFrame(self) @@ -103,6 +106,10 @@ class ErrorMessageBox(QtWidgets.QDialog): self._report_data = report_data self._content_widget = content_widget + @staticmethod + def convert_text_for_html(text): + return convert_text_for_html(text) + def _create_top_widget(self, parent_widget): label_widget = QtWidgets.QLabel(parent_widget) label_widget.setText( From acca8849a273daefe4b58567060edc4433ca96d2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 18:43:01 +0100 Subject: [PATCH 094/105] pass parent to creator error dialog --- openpype/tools/creator/window.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/creator/window.py b/openpype/tools/creator/window.py index dca1735121..22a6d5ce9c 100644 --- a/openpype/tools/creator/window.py +++ b/openpype/tools/creator/window.py @@ -445,7 +445,11 @@ class CreatorWindow(QtWidgets.QDialog): if error_info: box = CreateErrorMessageBox( - creator_plugin.family, subset_name, asset_name, *error_info + creator_plugin.family, + subset_name, + asset_name, + *error_info, + parent=self ) box.show() # Store dialog so is not garbage collected before is shown From 6cc944d46ca0cded5f6324f680fe1a4835279aef Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 18:43:15 +0100 Subject: [PATCH 095/105] use ErrorMessageBox in creator too --- openpype/tools/creator/widgets.py | 99 +++++++++++++++---------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index 89c90cc048..d2258a31c5 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -7,9 +7,10 @@ from avalon.vendor import qtawesome from openpype import style from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS +from openpype.tools.utils import ErrorMessageBox -class CreateErrorMessageBox(QtWidgets.QDialog): +class CreateErrorMessageBox(ErrorMessageBox): def __init__( self, family, @@ -17,23 +18,38 @@ class CreateErrorMessageBox(QtWidgets.QDialog): asset_name, exc_msg, formatted_traceback, - parent=None + parent ): - super(CreateErrorMessageBox, self).__init__(parent) - self.setWindowTitle("Creation failed") - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setWindowFlags( - self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint - ) + self._family = family + self._subset_name = subset_name + self._asset_name = asset_name + self._exc_msg = exc_msg + self._formatted_traceback = formatted_traceback + super(CreateErrorMessageBox, self).__init__("Creation failed", parent) - body_layout = QtWidgets.QVBoxLayout(self) - - main_label = ( + def _create_top_widget(self, parent_widget): + label_widget = QtWidgets.QLabel(parent_widget) + label_widget.setText( "Failed to create" ) - main_label_widget = QtWidgets.QLabel(main_label, self) - body_layout.addWidget(main_label_widget) + return label_widget + def _get_report_data(self): + report_message = ( + "Failed to create Subset: \"{subset}\" Family: \"{family}\"" + " in Asset: \"{asset}\"" + "\n\nError: {message}" + ).format( + subset=self._subset_name, + family=self._family, + asset=self._asset, + message=self._exc_msg + ) + if self._formatted_traceback: + report_message += "\n\n{}".format(self._formatted_traceback) + return [report_message] + + def _create_content(self, content_layout): item_name_template = ( "Family: {}
" "Subset: {}
" @@ -42,50 +58,29 @@ class CreateErrorMessageBox(QtWidgets.QDialog): exc_msg_template = "{}" line = self._create_line() - body_layout.addWidget(line) + content_layout.addWidget(line) - item_name = item_name_template.format(family, subset_name, asset_name) - item_name_widget = QtWidgets.QLabel( - item_name.replace("\n", "
"), self - ) - body_layout.addWidget(item_name_widget) - - exc_msg = exc_msg_template.format(exc_msg.replace("\n", "
")) - message_label_widget = QtWidgets.QLabel(exc_msg, self) - body_layout.addWidget(message_label_widget) - - if formatted_traceback: - tb_widget = QtWidgets.QLabel( - formatted_traceback.replace("\n", "
"), self + item_name_widget = QtWidgets.QLabel(self) + item_name_widget.setText( + self.convert_text_for_html( + item_name_template.format( + self._family, self._subset_name, self._asset_name + ) ) - tb_widget.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction - ) - body_layout.addWidget(tb_widget) - - footer_widget = QtWidgets.QWidget(self) - footer_layout = QtWidgets.QHBoxLayout(footer_widget) - button_box = QtWidgets.QDialogButtonBox(QtCore.Qt.Vertical) - button_box.setStandardButtons( - QtWidgets.QDialogButtonBox.StandardButton.Ok ) - button_box.accepted.connect(self._on_accept) - footer_layout.addWidget(button_box, alignment=QtCore.Qt.AlignRight) - body_layout.addWidget(footer_widget) + content_layout.addWidget(item_name_widget) - def showEvent(self, event): - self.setStyleSheet(style.load_stylesheet()) - super(CreateErrorMessageBox, self).showEvent(event) + message_label_widget = QtWidgets.QLabel(self) + message_label_widget.setText( + exc_msg_template.format(self.convert_text_for_html(self._exc_msg)) + ) + content_layout.addWidget(message_label_widget) - def _on_accept(self): - self.close() - - def _create_line(self): - line = QtWidgets.QFrame(self) - line.setFixedHeight(2) - line.setFrameShape(QtWidgets.QFrame.HLine) - line.setFrameShadow(QtWidgets.QFrame.Sunken) - return line + if self._formatted_traceback: + tb_widget = self._create_traceback_widget( + self._formatted_traceback + ) + content_layout.addWidget(tb_widget) class SubsetNameValidator(QtGui.QRegExpValidator): From e410addc46159f8f0c547d761890173548866dc5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 5 Jan 2022 18:50:09 +0100 Subject: [PATCH 096/105] minor fixes of dialog --- openpype/tools/creator/widgets.py | 10 +++++----- openpype/tools/utils/error_dialog.py | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openpype/tools/creator/widgets.py b/openpype/tools/creator/widgets.py index d2258a31c5..9dd435c1cc 100644 --- a/openpype/tools/creator/widgets.py +++ b/openpype/tools/creator/widgets.py @@ -42,7 +42,7 @@ class CreateErrorMessageBox(ErrorMessageBox): ).format( subset=self._subset_name, family=self._family, - asset=self._asset, + asset=self._asset_name, message=self._exc_msg ) if self._formatted_traceback: @@ -62,10 +62,8 @@ class CreateErrorMessageBox(ErrorMessageBox): item_name_widget = QtWidgets.QLabel(self) item_name_widget.setText( - self.convert_text_for_html( - item_name_template.format( - self._family, self._subset_name, self._asset_name - ) + item_name_template.format( + self._family, self._subset_name, self._asset_name ) ) content_layout.addWidget(item_name_widget) @@ -77,9 +75,11 @@ class CreateErrorMessageBox(ErrorMessageBox): content_layout.addWidget(message_label_widget) if self._formatted_traceback: + line_widget = self._create_line() tb_widget = self._create_traceback_widget( self._formatted_traceback ) + content_layout.addWidget(line_widget) content_layout.addWidget(tb_widget) diff --git a/openpype/tools/utils/error_dialog.py b/openpype/tools/utils/error_dialog.py index 0336f4bb08..f7b12bb69f 100644 --- a/openpype/tools/utils/error_dialog.py +++ b/openpype/tools/utils/error_dialog.py @@ -75,6 +75,9 @@ class ErrorMessageBox(QtWidgets.QDialog): content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(0, 0, 0, 0) + # Store content widget before creation of content + self._content_widget = content_widget + self._create_content(content_layout) content_layout.addStretch(1) @@ -104,7 +107,6 @@ class ErrorMessageBox(QtWidgets.QDialog): copy_report_btn.setVisible(False) self._report_data = report_data - self._content_widget = content_widget @staticmethod def convert_text_for_html(text): From 6f8700a5c5dd4535f8c18837949219aaadf224c9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 6 Jan 2022 12:46:05 +0100 Subject: [PATCH 097/105] OP-2282 - changes custom widget with CreatorError to standardize --- .../plugins/create/create_render.py | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index b796e9eaac..c73a1a1fc1 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,6 +1,7 @@ -import openpype.api -from Qt import QtWidgets from avalon import aftereffects +from avalon.api import CreatorError + +import openpype.api import logging @@ -27,14 +28,13 @@ class CreateRender(openpype.api.Creator): folders=False, footages=False) if len(items) > 1: - self._show_msg("Please select only single composition at time.") - return False + raise CreatorError("Please select only single " + "composition at time.") if not items: - self._show_msg("Nothing to create. Select composition " + - "if 'useSelection' or create at least " + - "one composition.") - return False + raise CreatorError("Nothing to create. Select composition " + + "if 'useSelection' or create at least " + + "one composition.") existing_subsets = [instance['subset'].lower() for instance in aftereffects.list_instances()] @@ -42,8 +42,7 @@ class CreateRender(openpype.api.Creator): item = items.pop() if self.name.lower() in existing_subsets: txt = "Instance with name \"{}\" already exists.".format(self.name) - self._show_msg(txt) - return False + raise CreatorError(txt) self.data["members"] = [item.id] self.data["uuid"] = item.id # for SubsetManager @@ -54,9 +53,3 @@ class CreateRender(openpype.api.Creator): stub.imprint(item, self.data) stub.set_label_color(item.id, 14) # Cyan options 0 - 16 stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"]) - - def _show_msg(self, txt): - msg = QtWidgets.QMessageBox() - msg.setIcon(QtWidgets.QMessageBox.Warning) - msg.setText(txt) - msg.exec_() From abc39a9e9f04d7efbda4b1548966794090c6f077 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jan 2022 15:17:02 +0100 Subject: [PATCH 098/105] make sure output is always the same --- openpype/lib/python_module_tools.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index 69da4cc661..903af01676 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -59,22 +59,23 @@ def modules_from_path(folder_path): """ crashed = [] modules = [] + output = (modules, crashed) # Just skip and return empty list if path is not set if not folder_path: - return modules + return output # Do not allow relative imports if folder_path.startswith("."): log.warning(( "BUG: Relative paths are not allowed for security reasons. {}" ).format(folder_path)) - return modules + return output folder_path = os.path.normpath(folder_path) if not os.path.isdir(folder_path): log.warning("Not a directory path: {}".format(folder_path)) - return modules + return output for filename in os.listdir(folder_path): # Ignore files which start with underscore @@ -101,7 +102,7 @@ def modules_from_path(folder_path): ) continue - return modules, crashed + return output def recursive_bases_from_class(klass): From 3f5b04161987ec15f937a5c0784b7389595209b3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jan 2022 15:17:18 +0100 Subject: [PATCH 099/105] update docstring --- openpype/lib/python_module_tools.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index 903af01676..f62c848e4a 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -49,13 +49,10 @@ def modules_from_path(folder_path): Arguments: path (str): Path to folder containing python scripts. - return_crasher (bool): Crashed module paths with exception info - will be returned too. Returns: - list, tuple: List of modules when `return_crashed` is False else tuple - with list of modules at first place and tuple of path and exception - info at second place. + tuple: First list contains successfully imported modules + and second list contains tuples of path and exception. """ crashed = [] modules = [] From e3122b9782fb1bb1ce17e6883a8d35a2c86ce12f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 6 Jan 2022 15:36:41 +0100 Subject: [PATCH 100/105] OP-2282 - added error when no layers in Background Background import without layers failed with nondescript message. Fixed broken layer naming --- .../aftereffects/plugins/load/load_background.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/load/load_background.py b/openpype/hosts/aftereffects/plugins/load/load_background.py index 9856abe3fe..4d3d46a442 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_background.py +++ b/openpype/hosts/aftereffects/plugins/load/load_background.py @@ -22,21 +22,23 @@ class BackgroundLoader(api.Loader): def load(self, context, name=None, namespace=None, data=None): items = stub.get_items(comps=True) - existing_items = [layer.name for layer in items] + existing_items = [layer.name.replace(stub.LOADED_ICON, '') + for layer in items] comp_name = get_unique_layer_name( existing_items, "{}_{}".format(context["asset"]["name"], name)) layers = get_background_layers(self.fname) + if not layers: + raise ValueError("No layers found in {}".format(self.fname)) + comp = stub.import_background(None, stub.LOADED_ICON + comp_name, layers) if not comp: - self.log.warning( - "Import background failed.") - self.log.warning("Check host app for alert error.") - return + raise ValueError("Import background failed. " + "Please contact support") self[:] = [comp] namespace = namespace or comp_name From ed5da3e0b04b28019e2b5c192364ced71d9a2c27 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jan 2022 16:39:59 +0100 Subject: [PATCH 101/105] expect that get_remote_versions may return None --- openpype/settings/entities/op_version_entity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/op_version_entity.py b/openpype/settings/entities/op_version_entity.py index 6184f7640a..782d65a446 100644 --- a/openpype/settings/entities/op_version_entity.py +++ b/openpype/settings/entities/op_version_entity.py @@ -72,6 +72,8 @@ class ProductionVersionsInputEntity(OpenPypeVersionInput): def _get_openpype_versions(self): versions = get_remote_versions(staging=False, production=True) + if versions is None: + return [] versions.append(get_installed_version()) return sorted(versions) @@ -82,4 +84,6 @@ class StagingVersionsInputEntity(OpenPypeVersionInput): def _get_openpype_versions(self): versions = get_remote_versions(staging=True, production=False) + if versions is None: + return [] return sorted(versions) From bbbfef4ca793765e0930f7f95af93f655303cbec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jan 2022 17:32:54 +0100 Subject: [PATCH 102/105] changed action/event paths to multiplatform and multipath inputs and changed labels --- .../defaults/system_settings/modules.json | 12 ++++++++++-- .../module_settings/schema_ftrack.json | 18 +++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index f0caa153de..b31dd6856c 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -15,8 +15,16 @@ "ftrack": { "enabled": true, "ftrack_server": "", - "ftrack_actions_path": [], - "ftrack_events_path": [], + "ftrack_actions_path": { + "windows": [], + "darwin": [], + "linux": [] + }, + "ftrack_events_path": { + "windows": [], + "darwin": [], + "linux": [] + }, "intent": { "items": { "-": "-", diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json index 5f659522c3..654ddf2938 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_ftrack.json @@ -21,19 +21,23 @@ }, { "type": "label", - "label": "Additional Ftrack paths" + "label": "Additional Ftrack event handlers paths" }, { - "type": "list", + "type": "path", "key": "ftrack_actions_path", - "label": "Action paths", - "object_type": "text" + "label": "User paths", + "use_label_wrap": true, + "multipath": true, + "multiplatform": true }, { - "type": "list", + "type": "path", "key": "ftrack_events_path", - "label": "Event paths", - "object_type": "text" + "label": "Server paths", + "use_label_wrap": true, + "multipath": true, + "multiplatform": true }, { "type": "separator" From 5ec147b336afe2499d428cbc81251b629d6e5ee5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jan 2022 17:42:36 +0100 Subject: [PATCH 103/105] ftrack module expect new type of path settings --- .../default_modules/ftrack/ftrack_module.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 8a7525d65b..e24869fd59 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -1,6 +1,7 @@ import os import json import collections +import platform import click @@ -42,18 +43,28 @@ class FtrackModule( self.ftrack_url = ftrack_url current_dir = os.path.dirname(os.path.abspath(__file__)) + low_platform = platform.system().lower() + server_event_handlers_paths = [ os.path.join(current_dir, "event_handlers_server") ] - server_event_handlers_paths.extend( - ftrack_settings["ftrack_events_path"] - ) + settings_server_paths = ftrack_settings["ftrack_events_path"] + if isinstance(settings_server_paths, dict): + settings_server_paths = settings_server_paths[low_platform] + + for path in settings_server_paths: + server_event_handlers_paths.append(path) + user_event_handlers_paths = [ os.path.join(current_dir, "event_handlers_user") ] - user_event_handlers_paths.extend( - ftrack_settings["ftrack_actions_path"] - ) + settings_action_paths = ftrack_settings["ftrack_actions_path"] + if isinstance(settings_action_paths, dict): + settings_action_paths = settings_action_paths[low_platform] + + for path in settings_action_paths: + user_event_handlers_paths.append(path) + # Prepare attribute self.server_event_handlers_paths = server_event_handlers_paths self.user_event_handlers_paths = user_event_handlers_paths From dd80f331f70ab7d51a32f4fab67871bb69e497ee Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jan 2022 17:42:51 +0100 Subject: [PATCH 104/105] paths to event handler are tried to format with environments --- .../modules/default_modules/ftrack/ftrack_module.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index e24869fd59..1b41159069 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -53,6 +53,11 @@ class FtrackModule( settings_server_paths = settings_server_paths[low_platform] for path in settings_server_paths: + # Try to format path with environments + try: + path = path.format(**os.environ) + except BaseException: + pass server_event_handlers_paths.append(path) user_event_handlers_paths = [ @@ -63,6 +68,11 @@ class FtrackModule( settings_action_paths = settings_action_paths[low_platform] for path in settings_action_paths: + # Try to format path with environments + try: + path = path.format(**os.environ) + except BaseException: + pass user_event_handlers_paths.append(path) # Prepare attribute From 85d973555b01d28f1b39f58f8f2bc93d2d229606 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 6 Jan 2022 17:44:41 +0100 Subject: [PATCH 105/105] try format the paths in ftrack event server (more dynamic) --- .../default_modules/ftrack/ftrack_module.py | 20 ++++--------------- .../ftrack/ftrack_server/ftrack_server.py | 6 ++++++ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/default_modules/ftrack/ftrack_module.py index 1b41159069..38ec02749a 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_module.py +++ b/openpype/modules/default_modules/ftrack/ftrack_module.py @@ -45,35 +45,23 @@ class FtrackModule( current_dir = os.path.dirname(os.path.abspath(__file__)) low_platform = platform.system().lower() + # Server event handler paths server_event_handlers_paths = [ os.path.join(current_dir, "event_handlers_server") ] settings_server_paths = ftrack_settings["ftrack_events_path"] if isinstance(settings_server_paths, dict): settings_server_paths = settings_server_paths[low_platform] + server_event_handlers_paths.extend(settings_server_paths) - for path in settings_server_paths: - # Try to format path with environments - try: - path = path.format(**os.environ) - except BaseException: - pass - server_event_handlers_paths.append(path) - + # User event handler paths user_event_handlers_paths = [ os.path.join(current_dir, "event_handlers_user") ] settings_action_paths = ftrack_settings["ftrack_actions_path"] if isinstance(settings_action_paths, dict): settings_action_paths = settings_action_paths[low_platform] - - for path in settings_action_paths: - # Try to format path with environments - try: - path = path.format(**os.environ) - except BaseException: - pass - user_event_handlers_paths.append(path) + user_event_handlers_paths.extend(settings_action_paths) # Prepare attribute self.server_event_handlers_paths = server_event_handlers_paths diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/ftrack_server.py b/openpype/modules/default_modules/ftrack/ftrack_server/ftrack_server.py index bd67fba3d6..8944591b71 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_server/ftrack_server.py +++ b/openpype/modules/default_modules/ftrack/ftrack_server/ftrack_server.py @@ -63,6 +63,12 @@ class FtrackServer: # Iterate all paths register_functions = [] for path in paths: + # Try to format path with environments + try: + path = path.format(**os.environ) + except BaseException: + pass + # Get all modules with functions modules, crashed = modules_from_path(path) for filepath, exc_info in crashed: