diff --git a/client/ayon_core/__init__.py b/client/ayon_core/__init__.py index ce5a28601c..6cde11c822 100644 --- a/client/ayon_core/__init__.py +++ b/client/ayon_core/__init__.py @@ -9,10 +9,6 @@ AYON_CORE_ROOT = os.path.dirname(os.path.abspath(__file__)) # ------------------------- PACKAGE_DIR = AYON_CORE_ROOT PLUGINS_DIR = os.path.join(AYON_CORE_ROOT, "plugins") -AYON_SERVER_ENABLED = True - -# Indicate if AYON entities should be used instead of OpenPype entities -USE_AYON_ENTITIES = True # ------------------------- @@ -23,6 +19,4 @@ __all__ = ( "AYON_CORE_ROOT", "PACKAGE_DIR", "PLUGINS_DIR", - "AYON_SERVER_ENABLED", - "USE_AYON_ENTITIES", ) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 7f0636ccca..982626ad9d 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -36,9 +36,6 @@ IGNORED_FILENAMES = { # Files ignored on addons import from "./ayon_core/modules" IGNORED_DEFAULT_FILENAMES = { "__init__.py", - "base.py", - "interfaces.py", - "click_wrap.py", } # When addon was moved from ayon-core codebase @@ -124,77 +121,10 @@ class ProcessContext: print(f"Unknown keys in ProcessContext: {unknown_keys}") -# Inherit from `object` for Python 2 hosts -class _ModuleClass(object): - """Fake module class for storing AYON addons. - - Object of this class can be stored to `sys.modules` and used for storing - dynamically imported modules. - """ - - def __init__(self, name): - # Call setattr on super class - super(_ModuleClass, self).__setattr__("name", name) - super(_ModuleClass, self).__setattr__("__name__", name) - - # Where modules and interfaces are stored - super(_ModuleClass, self).__setattr__("__attributes__", dict()) - super(_ModuleClass, self).__setattr__("__defaults__", set()) - - super(_ModuleClass, self).__setattr__("_log", None) - - def __getattr__(self, attr_name): - if attr_name not in self.__attributes__: - if attr_name in ("__path__", "__file__"): - return None - raise AttributeError("'{}' has not attribute '{}'".format( - self.name, attr_name - )) - return self.__attributes__[attr_name] - - def __iter__(self): - for module in self.values(): - yield module - - def __setattr__(self, attr_name, value): - if attr_name in self.__attributes__: - self.log.warning( - "Duplicated name \"{}\" in {}. Overriding.".format( - attr_name, self.name - ) - ) - self.__attributes__[attr_name] = value - - def __setitem__(self, key, value): - self.__setattr__(key, value) - - def __getitem__(self, key): - return getattr(self, key) - - @property - def log(self): - if self._log is None: - super(_ModuleClass, self).__setattr__( - "_log", Logger.get_logger(self.name) - ) - return self._log - - def get(self, key, default=None): - return self.__attributes__.get(key, default) - - def keys(self): - return self.__attributes__.keys() - - def values(self): - return self.__attributes__.values() - - def items(self): - return self.__attributes__.items() - - class _LoadCache: addons_lock = threading.Lock() addons_loaded = False + addon_modules = [] def load_addons(force=False): @@ -308,7 +238,7 @@ def _handle_moved_addons(addon_name, milestone_version, log): return addon_dir -def _load_ayon_addons(openpype_modules, modules_key, log): +def _load_ayon_addons(log): """Load AYON addons based on information from server. This function should not trigger downloading of any addons but only use @@ -316,23 +246,14 @@ def _load_ayon_addons(openpype_modules, modules_key, log): development). Args: - openpype_modules (_ModuleClass): Module object where modules are - stored. - modules_key (str): Key under which will be modules imported in - `sys.modules`. log (logging.Logger): Logger object. - Returns: - List[str]: List of v3 addons to skip to load because v4 alternative is - imported. """ - - addons_to_skip_in_core = [] - + all_addon_modules = [] bundle_info = _get_ayon_bundle_data() addons_info = _get_ayon_addons_information(bundle_info) if not addons_info: - return addons_to_skip_in_core + return all_addon_modules addons_dir = os.environ.get("AYON_ADDONS_DIR") if not addons_dir: @@ -355,7 +276,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log): addon_version = addon_info["version"] # core addon does not have any addon object - if addon_name in ("openpype", "core"): + if addon_name == "core": continue dev_addon_info = dev_addons_info.get(addon_name, {}) @@ -394,7 +315,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log): continue sys.path.insert(0, addon_dir) - imported_modules = [] + addon_modules = [] for name in os.listdir(addon_dir): # Ignore of files is implemented to be able to run code from code # where usually is more files than just the addon @@ -421,7 +342,7 @@ def _load_ayon_addons(openpype_modules, modules_key, log): inspect.isclass(attr) and issubclass(attr, AYONAddon) ): - imported_modules.append(mod) + addon_modules.append(mod) break except BaseException: @@ -430,50 +351,37 @@ def _load_ayon_addons(openpype_modules, modules_key, log): exc_info=True ) - if not imported_modules: + if not addon_modules: log.warning("Addon {} {} has no content to import".format( addon_name, addon_version )) continue - if len(imported_modules) > 1: + if len(addon_modules) > 1: log.warning(( - "Skipping addon '{}'." - " Multiple modules were found ({}) in dir {}." + "Multiple modules ({}) were found in addon '{}' in dir {}." ).format( + ", ".join([m.__name__ for m in addon_modules]), addon_name, - ", ".join([m.__name__ for m in imported_modules]), addon_dir, )) - continue + all_addon_modules.extend(addon_modules) - mod = imported_modules[0] - addon_alias = getattr(mod, "V3_ALIAS", None) - if not addon_alias: - addon_alias = addon_name - addons_to_skip_in_core.append(addon_alias) - new_import_str = "{}.{}".format(modules_key, addon_alias) - - sys.modules[new_import_str] = mod - setattr(openpype_modules, addon_alias, mod) - - return addons_to_skip_in_core + return all_addon_modules -def _load_addons_in_core( - ignore_addon_names, openpype_modules, modules_key, log -): +def _load_addons_in_core(log): # Add current directory at first place # - has small differences in import logic + addon_modules = [] modules_dir = os.path.join(AYON_CORE_ROOT, "modules") if not os.path.exists(modules_dir): log.warning( f"Could not find path when loading AYON addons \"{modules_dir}\"" ) - return + return addon_modules ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES - for filename in os.listdir(modules_dir): # Ignore filenames if filename in ignored_filenames: @@ -482,9 +390,6 @@ def _load_addons_in_core( fullpath = os.path.join(modules_dir, filename) basename, ext = os.path.splitext(filename) - if basename in ignore_addon_names: - continue - # Validations if os.path.isdir(fullpath): # Check existence of init file @@ -503,69 +408,43 @@ def _load_addons_in_core( # - check manifest and content of manifest try: # Don't import dynamically current directory modules - new_import_str = f"{modules_key}.{basename}" - import_str = f"ayon_core.modules.{basename}" default_module = __import__(import_str, fromlist=("", )) - sys.modules[new_import_str] = default_module - setattr(openpype_modules, basename, default_module) + addon_modules.append(default_module) except Exception: log.error( f"Failed to import in-core addon '{basename}'.", exc_info=True ) + return addon_modules def _load_addons(): - # Key under which will be modules imported in `sys.modules` - modules_key = "openpype_modules" - - # Change `sys.modules` - sys.modules[modules_key] = openpype_modules = _ModuleClass(modules_key) - log = Logger.get_logger("AddonsLoader") - ignore_addon_names = _load_ayon_addons( - openpype_modules, modules_key, log - ) - _load_addons_in_core( - ignore_addon_names, openpype_modules, modules_key, log - ) + addon_modules = _load_ayon_addons(log) + # All addon in 'modules' folder are tray actions and should be moved + # to tray tool. + # TODO remove + addon_modules.extend(_load_addons_in_core(log)) - -_MARKING_ATTR = "_marking" -def mark_func(func): - """Mark function to be used in report. - - Args: - func (Callable): Function to mark. - - Returns: - Callable: Marked function. - """ - - setattr(func, _MARKING_ATTR, True) - return func - - -def is_func_marked(func): - return getattr(func, _MARKING_ATTR, False) + # Store modules to local cache + _LoadCache.addon_modules = addon_modules class AYONAddon(ABC): """Base class of AYON addon. Attributes: - id (UUID): Addon object id. enabled (bool): Is addon enabled. name (str): Addon name. Args: manager (AddonsManager): Manager object who discovered addon. settings (dict[str, Any]): AYON settings. - """ + """ enabled = True _id = None @@ -585,8 +464,8 @@ class AYONAddon(ABC): Returns: str: Object id. - """ + """ if self._id is None: self._id = uuid4() return self._id @@ -598,8 +477,8 @@ class AYONAddon(ABC): Returns: str: Addon name. - """ + """ pass @property @@ -630,16 +509,16 @@ class AYONAddon(ABC): Args: settings (dict[str, Any]): Settings. - """ + """ pass - @mark_func def connect_with_addons(self, enabled_addons): """Connect with other enabled addons. Args: enabled_addons (list[AYONAddon]): Addons that are enabled. + """ pass @@ -673,8 +552,8 @@ class AYONAddon(ABC): Returns: dict[str, str]: Environment variables. - """ + """ return {} def modify_application_launch_arguments(self, application, env): @@ -686,8 +565,8 @@ class AYONAddon(ABC): Args: application (Application): Application that is launched. env (dict[str, str]): Current environment variables. - """ + """ pass def on_host_install(self, host, host_name, project_name): @@ -706,8 +585,8 @@ class AYONAddon(ABC): host_name (str): Name of host. project_name (str): Project name which is main part of host context. - """ + """ pass def cli(self, addon_click_group): @@ -734,31 +613,11 @@ class AYONAddon(ABC): Args: addon_click_group (click.Group): Group to which can be added commands. + """ - pass -class OpenPypeModule(AYONAddon): - """Base class of OpenPype module. - - Deprecated: - Use `AYONAddon` instead. - - Args: - manager (AddonsManager): Manager object who discovered addon. - settings (dict[str, Any]): Module settings (OpenPype settings). - """ - - # Disable by default - enabled = False - - -class OpenPypeAddOn(OpenPypeModule): - # Enable Addon by default - enabled = True - - class _AddonReportInfo: def __init__( self, class_name, name, version, report_value_by_label @@ -790,8 +649,8 @@ class AddonsManager: settings (Optional[dict[str, Any]]): AYON studio settings. initialize (Optional[bool]): Initialize addons on init. True by default. - """ + """ # Helper attributes for report _report_total_key = "Total" _log = None @@ -827,8 +686,8 @@ class AddonsManager: Returns: Union[AYONAddon, Any]: Addon found by name or `default`. - """ + """ return self._addons_by_name.get(addon_name, default) @property @@ -855,8 +714,8 @@ class AddonsManager: Returns: Union[AYONAddon, None]: Enabled addon found by name or None. - """ + """ addon = self.get(addon_name) if addon is not None and addon.enabled: return addon @@ -867,8 +726,8 @@ class AddonsManager: Returns: list[AYONAddon]: Initialized and enabled addons. - """ + """ return [ addon for addon in self._addons @@ -880,8 +739,6 @@ class AddonsManager: # Make sure modules are loaded load_addons() - import openpype_modules - self.log.debug("*** AYON addons initialization.") # Prepare settings for addons @@ -889,14 +746,12 @@ class AddonsManager: if settings is None: settings = get_studio_settings() - modules_settings = {} - report = {} time_start = time.time() prev_start_time = time_start addon_classes = [] - for module in openpype_modules: + for module in _LoadCache.addon_modules: # Go through globals in `ayon_core.modules` for name in dir(module): modules_item = getattr(module, name, None) @@ -905,8 +760,6 @@ class AddonsManager: if ( not inspect.isclass(modules_item) or modules_item is AYONAddon - or modules_item is OpenPypeModule - or modules_item is OpenPypeAddOn or not issubclass(modules_item, AYONAddon) ): continue @@ -932,33 +785,14 @@ class AddonsManager: addon_classes.append(modules_item) - aliased_names = [] for addon_cls in addon_classes: name = addon_cls.__name__ - if issubclass(addon_cls, OpenPypeModule): - # TODO change to warning - self.log.debug(( - "Addon '{}' is inherited from 'OpenPypeModule'." - " Please use 'AYONAddon'." - ).format(name)) - try: - # Try initialize module - if issubclass(addon_cls, OpenPypeModule): - addon = addon_cls(self, modules_settings) - else: - addon = addon_cls(self, settings) + addon = addon_cls(self, settings) # Store initialized object self._addons.append(addon) self._addons_by_id[addon.id] = addon self._addons_by_name[addon.name] = addon - # NOTE This will be removed with release 1.0.0 of ayon-core - # please use carefully. - # Gives option to use alias name for addon for cases when - # name in OpenPype was not the same as in AYON. - name_alias = getattr(addon, "openpype_alias", None) - if name_alias: - aliased_names.append((name_alias, addon)) now = time.time() report[addon.__class__.__name__] = now - prev_start_time @@ -977,17 +811,6 @@ class AddonsManager: f"[{enabled_str}] {addon.name} ({addon.version})" ) - for item in aliased_names: - name_alias, addon = item - if name_alias not in self._addons_by_name: - self._addons_by_name[name_alias] = addon - continue - self.log.warning( - "Alias name '{}' of addon '{}' is already assigned.".format( - name_alias, addon.name - ) - ) - if self._report is not None: report[self._report_total_key] = time.time() - time_start self._report["Initialization"] = report @@ -1004,16 +827,7 @@ class AddonsManager: self.log.debug("Has {} enabled addons.".format(len(enabled_addons))) for addon in enabled_addons: try: - if not is_func_marked(addon.connect_with_addons): - addon.connect_with_addons(enabled_addons) - - elif hasattr(addon, "connect_with_modules"): - self.log.warning(( - "DEPRECATION WARNING: Addon '{}' still uses" - " 'connect_with_modules' method. Please switch to use" - " 'connect_with_addons' method." - ).format(addon.name)) - addon.connect_with_modules(enabled_addons) + addon.connect_with_addons(enabled_addons) except Exception: self.log.error( @@ -1362,56 +1176,3 @@ class AddonsManager: # Join rows with newline char and add new line at the end output = "\n".join(formatted_rows) + "\n" print(output) - - # DEPRECATED - Module compatibility - @property - def modules(self): - self.log.warning( - "DEPRECATION WARNING: Used deprecated property" - " 'modules' please use 'addons' instead." - ) - return self.addons - - @property - def modules_by_id(self): - self.log.warning( - "DEPRECATION WARNING: Used deprecated property" - " 'modules_by_id' please use 'addons_by_id' instead." - ) - return self.addons_by_id - - @property - def modules_by_name(self): - self.log.warning( - "DEPRECATION WARNING: Used deprecated property" - " 'modules_by_name' please use 'addons_by_name' instead." - ) - return self.addons_by_name - - def get_enabled_module(self, *args, **kwargs): - self.log.warning( - "DEPRECATION WARNING: Used deprecated method" - " 'get_enabled_module' please use 'get_enabled_addon' instead." - ) - return self.get_enabled_addon(*args, **kwargs) - - def initialize_modules(self): - self.log.warning( - "DEPRECATION WARNING: Used deprecated method" - " 'initialize_modules' please use 'initialize_addons' instead." - ) - self.initialize_addons() - - def get_enabled_modules(self): - self.log.warning( - "DEPRECATION WARNING: Used deprecated method" - " 'get_enabled_modules' please use 'get_enabled_addons' instead." - ) - return self.get_enabled_addons() - - def get_host_module(self, host_name): - self.log.warning( - "DEPRECATION WARNING: Used deprecated method" - " 'get_host_module' please use 'get_host_addon' instead." - ) - return self.get_host_addon(host_name) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index db6674d88f..b80b243db2 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -21,21 +21,7 @@ from ayon_core.lib import ( -class AliasedGroup(click.Group): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._aliases = {} - - def set_alias(self, src_name, dst_name): - self._aliases[dst_name] = src_name - - def get_command(self, ctx, cmd_name): - if cmd_name in self._aliases: - cmd_name = self._aliases[cmd_name] - return super().get_command(ctx, cmd_name) - - -@click.group(cls=AliasedGroup, invoke_without_command=True) +@click.group(invoke_without_command=True) @click.pass_context @click.option("--use-staging", is_flag=True, expose_value=False, help="use staging variants") @@ -86,10 +72,6 @@ def addon(ctx): pass -# Add 'addon' as alias for module -main_cli.set_alias("addon", "module") - - @main_cli.command() @click.pass_context @click.argument("output_json_path") diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index 0074c4d2bd..03ed574081 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -7,13 +7,10 @@ from .local_settings import ( JSONSettingRegistry, AYONSecureRegistry, AYONSettingsRegistry, - OpenPypeSecureRegistry, - OpenPypeSettingsRegistry, get_launcher_local_dir, get_launcher_storage_dir, get_local_site_id, get_ayon_username, - get_openpype_username, ) from .ayon_connection import initialize_ayon_connection from .cache import ( @@ -59,13 +56,11 @@ from .env_tools import ( from .terminal import Terminal from .execute import ( get_ayon_launcher_args, - get_openpype_execute_args, get_linux_launcher_args, execute, run_subprocess, run_detached_process, run_ayon_launcher_process, - run_openpype_process, path_to_subprocess_arg, CREATE_NO_WINDOW ) @@ -145,13 +140,10 @@ __all__ = [ "JSONSettingRegistry", "AYONSecureRegistry", "AYONSettingsRegistry", - "OpenPypeSecureRegistry", - "OpenPypeSettingsRegistry", "get_launcher_local_dir", "get_launcher_storage_dir", "get_local_site_id", "get_ayon_username", - "get_openpype_username", "initialize_ayon_connection", @@ -162,13 +154,11 @@ __all__ = [ "register_event_callback", "get_ayon_launcher_args", - "get_openpype_execute_args", "get_linux_launcher_args", "execute", "run_subprocess", "run_detached_process", "run_ayon_launcher_process", - "run_openpype_process", "path_to_subprocess_arg", "CREATE_NO_WINDOW", diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 7e022f6dba..894b012d59 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -4,7 +4,7 @@ import collections import uuid import json import copy -from abc import ABCMeta, abstractmethod, abstractproperty +from abc import ABCMeta, abstractmethod import clique @@ -16,7 +16,7 @@ _attr_defs_by_type = {} def register_attr_def_class(cls): """Register attribute definition. - Currently are registered definitions used to deserialize data to objects. + Currently registered definitions are used to deserialize data to objects. Attrs: cls (AbstractAttrDef): Non-abstract class to be registered with unique @@ -60,7 +60,7 @@ def get_default_values(attribute_definitions): for which default values should be collected. Returns: - Dict[str, Any]: Default values for passet attribute definitions. + Dict[str, Any]: Default values for passed attribute definitions. """ output = {} @@ -75,13 +75,13 @@ def get_default_values(attribute_definitions): class AbstractAttrDefMeta(ABCMeta): - """Metaclass to validate existence of 'key' attribute. + """Metaclass to validate the existence of 'key' attribute. - Each object of `AbstractAttrDef` mus have defined 'key' attribute. + Each object of `AbstractAttrDef` must have defined 'key' attribute. """ - def __call__(self, *args, **kwargs): - obj = super(AbstractAttrDefMeta, self).__call__(*args, **kwargs) + def __call__(cls, *args, **kwargs): + obj = super(AbstractAttrDefMeta, cls).__call__(*args, **kwargs) init_class = getattr(obj, "__init__class__", None) if init_class is not AbstractAttrDef: raise TypeError("{} super was not called in __init__.".format( @@ -162,7 +162,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def __ne__(self, other): return not self.__eq__(other) - @abstractproperty + @property + @abstractmethod def type(self): """Attribute definition type also used as identifier of class. @@ -215,7 +216,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): # ----------------------------------------- -# UI attribute definitoins won't hold value +# UI attribute definitions won't hold value # ----------------------------------------- class UIDef(AbstractAttrDef): @@ -245,7 +246,7 @@ class UILabelDef(UIDef): # --------------------------------------- -# Attribute defintioins should hold value +# Attribute definitions should hold value # --------------------------------------- class UnknownDef(AbstractAttrDef): @@ -311,7 +312,7 @@ class NumberDef(AbstractAttrDef): ): minimum = 0 if minimum is None else minimum maximum = 999999 if maximum is None else maximum - # Swap min/max when are passed in opposited order + # Swap min/max when are passed in opposite order if minimum > maximum: maximum, minimum = minimum, maximum @@ -364,10 +365,10 @@ class NumberDef(AbstractAttrDef): class TextDef(AbstractAttrDef): """Text definition. - Text can have multiline option so endline characters are allowed regex + Text can have multiline option so end-line characters are allowed regex validation can be applied placeholder for UI purposes and default value. - Regex validation is not part of attribute implemntentation. + Regex validation is not part of attribute implementation. Args: multiline(bool): Text has single or multiline support. @@ -949,7 +950,8 @@ def deserialize_attr_def(attr_def_data): """Deserialize attribute definition from data. Args: - attr_def (Dict[str, Any]): Attribute definition data to deserialize. + attr_def_data (Dict[str, Any]): Attribute definition data to + deserialize. """ attr_type = attr_def_data.pop("type") diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 4e6cb415e7..95696fd272 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -235,26 +235,6 @@ def run_ayon_launcher_process(*args, add_sys_paths=False, **kwargs): return run_subprocess(args, env=env, **kwargs) -def run_openpype_process(*args, **kwargs): - """Execute AYON process with passed arguments and wait. - - Wrapper for 'run_process' which prepends AYON executable arguments - before passed arguments and define environments if are not passed. - - Values from 'os.environ' are used for environments if are not passed. - They are cleaned using 'clean_envs_for_ayon_process' function. - - Example: - >>> run_openpype_process("version") - - Args: - *args (tuple): AYON cli arguments. - **kwargs (dict): Keyword arguments for subprocess.Popen. - - """ - return run_ayon_launcher_process(*args, **kwargs) - - def run_detached_process(args, **kwargs): """Execute process with passed arguments as separated process. @@ -341,14 +321,12 @@ def path_to_subprocess_arg(path): def get_ayon_launcher_args(*args): - """Arguments to run ayon-launcher process. + """Arguments to run AYON launcher process. - Arguments for subprocess when need to spawn new pype process. Which may be - needed when new python process for pype scripts must be executed in build - pype. + Arguments for subprocess when need to spawn new AYON launcher process. Reasons: - Ayon-launcher started from code has different executable set to + AYON launcher started from code has different executable set to virtual env python and must have path to script as first argument which is not needed for built application. @@ -356,7 +334,8 @@ def get_ayon_launcher_args(*args): *args (str): Any arguments that will be added after executables. Returns: - list[str]: List of arguments to run ayon-launcher process. + list[str]: List of arguments to run AYON launcher process. + """ executable = os.environ["AYON_EXECUTABLE"] launch_args = [executable] @@ -414,21 +393,3 @@ def get_linux_launcher_args(*args): launch_args.extend(args) return launch_args - - -def get_openpype_execute_args(*args): - """Arguments to run pype command. - - Arguments for subprocess when need to spawn new pype process. Which may be - needed when new python process for pype scripts must be executed in build - pype. - - ## Why is this needed? - Pype executed from code has different executable set to virtual env python - and must have path to script as first argument which is not needed for - build pype. - - It is possible to pass any arguments that will be added after pype - executables. - """ - return get_ayon_launcher_args(*args) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 5f49dd512c..690781151c 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -584,11 +584,3 @@ def get_ayon_username(): """ return ayon_api.get_user()["name"] - - -def get_openpype_username(): - return get_ayon_username() - - -OpenPypeSecureRegistry = AYONSecureRegistry -OpenPypeSettingsRegistry = AYONSettingsRegistry diff --git a/client/ayon_core/modules/__init__.py b/client/ayon_core/modules/__init__.py index f4e381f4a0..e69de29bb2 100644 --- a/client/ayon_core/modules/__init__.py +++ b/client/ayon_core/modules/__init__.py @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -from . import click_wrap -from .interfaces import ( - IPluginPaths, - ITrayAddon, - ITrayModule, - ITrayAction, - ITrayService, - IHostAddon, -) - -from .base import ( - AYONAddon, - OpenPypeModule, - OpenPypeAddOn, - - load_modules, - - ModulesManager, -) - - -__all__ = ( - "click_wrap", - - "IPluginPaths", - "ITrayAddon", - "ITrayModule", - "ITrayAction", - "ITrayService", - "IHostAddon", - - "AYONAddon", - "OpenPypeModule", - "OpenPypeAddOn", - - "load_modules", - - "ModulesManager", -) diff --git a/client/ayon_core/modules/base.py b/client/ayon_core/modules/base.py deleted file mode 100644 index df412d141e..0000000000 --- a/client/ayon_core/modules/base.py +++ /dev/null @@ -1,25 +0,0 @@ -# Backwards compatibility support -# - TODO should be removed before release 1.0.0 -from ayon_core.addon import ( - AYONAddon, - AddonsManager, - load_addons, -) -from ayon_core.addon.base import ( - OpenPypeModule, - OpenPypeAddOn, -) - -ModulesManager = AddonsManager -load_modules = load_addons - - -__all__ = ( - "AYONAddon", - "AddonsManager", - "load_addons", - "OpenPypeModule", - "OpenPypeAddOn", - "ModulesManager", - "load_modules", -) diff --git a/client/ayon_core/modules/click_wrap.py b/client/ayon_core/modules/click_wrap.py deleted file mode 100644 index 8f68de187a..0000000000 --- a/client/ayon_core/modules/click_wrap.py +++ /dev/null @@ -1 +0,0 @@ -from ayon_core.addon.click_wrap import * diff --git a/client/ayon_core/modules/interfaces.py b/client/ayon_core/modules/interfaces.py deleted file mode 100644 index 4b114b7a0e..0000000000 --- a/client/ayon_core/modules/interfaces.py +++ /dev/null @@ -1,21 +0,0 @@ -from ayon_core.addon.interfaces import ( - IPluginPaths, - ITrayAddon, - ITrayAction, - ITrayService, - IHostAddon, -) - -ITrayModule = ITrayAddon -ILaunchHookPaths = object - - -__all__ = ( - "IPluginPaths", - "ITrayAddon", - "ITrayAction", - "ITrayService", - "IHostAddon", - "ITrayModule", - "ILaunchHookPaths", -) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index 8fd00ee6b6..e0b7d4acae 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -55,7 +55,6 @@ from .publish import ( PublishXmlValidationError, KnownPublishError, AYONPyblishPluginMixin, - OpenPypePyblishPluginMixin, OptionalPyblishPluginMixin, ) @@ -77,7 +76,6 @@ from .actions import ( from .context_tools import ( install_ayon_plugins, - install_openpype_plugins, install_host, uninstall_host, is_installed, @@ -168,7 +166,6 @@ __all__ = ( "PublishXmlValidationError", "KnownPublishError", "AYONPyblishPluginMixin", - "OpenPypePyblishPluginMixin", "OptionalPyblishPluginMixin", # --- Actions --- @@ -187,7 +184,6 @@ __all__ = ( # --- Process context --- "install_ayon_plugins", - "install_openpype_plugins", "install_host", "uninstall_host", "is_installed", diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 8b72405048..5c14cf20e6 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -234,16 +234,6 @@ def install_ayon_plugins(project_name=None, host_name=None): register_inventory_action_path(path) -def install_openpype_plugins(project_name=None, host_name=None): - """Install AYON core plugins and make sure the core is initialized. - - Deprecated: - Use `install_ayon_plugins` instead. - - """ - install_ayon_plugins(project_name, host_name) - - def uninstall_host(): """Undo all of what `install()` did""" host = registered_host() diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3f067427fa..7706860499 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -6,7 +6,8 @@ import traceback import collections import inspect from contextlib import contextmanager -from typing import Optional +import typing +from typing import Optional, Iterable, Dict import pyblish.logic import pyblish.api @@ -31,13 +32,15 @@ from .exceptions import ( HostMissRequiredMethod, ) from .changes import TrackChangesItem -from .structures import PublishAttributes, ConvertorItem +from .structures import PublishAttributes, ConvertorItem, InstanceContextInfo from .creator_plugins import ( Creator, AutoCreator, discover_creator_plugins, discover_convertor_plugins, ) +if typing.TYPE_CHECKING: + from .structures import CreatedInstance # Import of functions and classes that were moved to different file # TODO Should be removed in future release - Added 24/08/28, 0.4.3-dev.1 @@ -183,6 +186,10 @@ class CreateContext: # Shared data across creators during collection phase self._collection_shared_data = None + # Context validation cache + self._folder_id_by_folder_path = {} + self._task_names_by_folder_path = {} + self.thumbnail_paths_by_instance_id = {} # Trigger reset if was enabled @@ -202,17 +209,19 @@ class CreateContext: """Access to global publish attributes.""" return self._publish_attributes - def get_instance_by_id(self, instance_id): + def get_instance_by_id( + self, instance_id: str + ) -> Optional["CreatedInstance"]: """Receive instance by id. Args: instance_id (str): Instance id. Returns: - Union[CreatedInstance, None]: Instance or None if instance with + Optional[CreatedInstance]: Instance or None if instance with given id is not available. - """ + """ return self._instances_by_id.get(instance_id) def get_sorted_creators(self, identifiers=None): @@ -224,8 +233,8 @@ class CreateContext: Returns: List[BaseCreator]: Sorted creator plugins by 'order' value. - """ + """ if identifiers is not None: identifiers = set(identifiers) creators = [ @@ -491,6 +500,8 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} + self._folder_id_by_folder_path = {} + self._task_names_by_folder_path = {} def reset_finalization(self): """Cleanup of attributes after reset.""" @@ -715,7 +726,7 @@ class CreateContext: self._original_context_data, self.context_data_to_store() ) - def creator_adds_instance(self, instance): + def creator_adds_instance(self, instance: "CreatedInstance"): """Creator adds new instance to context. Instances should be added only from creators. @@ -942,7 +953,7 @@ class CreateContext: def _remove_instance(self, instance): self._instances_by_id.pop(instance.id, None) - def creator_removed_instance(self, instance): + def creator_removed_instance(self, instance: "CreatedInstance"): """When creator removes instance context should be acknowledged. If creator removes instance conext should know about it to avoid @@ -990,7 +1001,7 @@ class CreateContext: [], self._bulk_instances_to_process ) - self.validate_instances_context(instances_to_validate) + self.get_instances_context_info(instances_to_validate) def reset_instances(self): """Reload instances""" @@ -1079,26 +1090,70 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) - def validate_instances_context(self, instances=None): - """Validate 'folder' and 'task' instance context.""" + def get_instances_context_info( + self, instances: Optional[Iterable["CreatedInstance"]] = None + ) -> Dict[str, InstanceContextInfo]: + """Validate 'folder' and 'task' instance context. + + Args: + instances (Optional[Iterable[CreatedInstance]]): Instances to + validate. If not provided all instances are validated. + + Returns: + Dict[str, InstanceContextInfo]: Validation results by instance id. + + """ # Use all instances from context if 'instances' are not passed if instances is None: - instances = tuple(self._instances_by_id.values()) + instances = self._instances_by_id.values() + instances = tuple(instances) + info_by_instance_id = { + instance.id: InstanceContextInfo( + instance.get("folderPath"), + instance.get("task"), + False, + False, + ) + for instance in instances + } # Skip if instances are empty - if not instances: - return + if not info_by_instance_id: + return info_by_instance_id project_name = self.project_name - task_names_by_folder_path = {} + to_validate = [] + task_names_by_folder_path = collections.defaultdict(set) for instance in instances: - folder_path = instance.get("folderPath") - task_name = instance.get("task") - if folder_path: - task_names_by_folder_path[folder_path] = set() - if task_name: - task_names_by_folder_path[folder_path].add(task_name) + context_info = info_by_instance_id[instance.id] + if instance.has_promised_context: + context_info.folder_is_valid = True + context_info.task_is_valid = True + continue + # TODO allow context promise + folder_path = context_info.folder_path + if not folder_path: + continue + + if folder_path in self._folder_id_by_folder_path: + folder_id = self._folder_id_by_folder_path[folder_path] + if folder_id is None: + continue + context_info.folder_is_valid = True + + task_name = context_info.task_name + if task_name is not None: + tasks_cache = self._task_names_by_folder_path.get(folder_path) + if tasks_cache is not None: + context_info.task_is_valid = task_name in tasks_cache + continue + + to_validate.append(instance) + task_names_by_folder_path[folder_path].add(task_name) + + if not to_validate: + return info_by_instance_id # Backwards compatibility for cases where folder name is set instead # of folder path @@ -1120,7 +1175,9 @@ class CreateContext: fields={"id", "path"} ): folder_id = folder_entity["id"] - folder_paths_by_id[folder_id] = folder_entity["path"] + folder_path = folder_entity["path"] + folder_paths_by_id[folder_id] = folder_path + self._folder_id_by_folder_path[folder_path] = folder_id folder_entities_by_name = collections.defaultdict(list) if folder_names: @@ -1131,8 +1188,10 @@ class CreateContext: ): folder_id = folder_entity["id"] folder_name = folder_entity["name"] - folder_paths_by_id[folder_id] = folder_entity["path"] + folder_path = folder_entity["path"] + folder_paths_by_id[folder_id] = folder_path folder_entities_by_name[folder_name].append(folder_entity) + self._folder_id_by_folder_path[folder_path] = folder_id tasks_entities = ayon_api.get_tasks( project_name, @@ -1145,12 +1204,11 @@ class CreateContext: folder_id = task_entity["folderId"] folder_path = folder_paths_by_id[folder_id] task_names_by_folder_path[folder_path].add(task_entity["name"]) + self._task_names_by_folder_path.update(task_names_by_folder_path) - for instance in instances: - if not instance.has_valid_folder or not instance.has_valid_task: - continue - + for instance in to_validate: folder_path = instance["folderPath"] + task_name = instance.get("task") if folder_path and "/" not in folder_path: folder_entities = folder_entities_by_name.get(folder_path) if len(folder_entities) == 1: @@ -1158,15 +1216,16 @@ class CreateContext: instance["folderPath"] = folder_path if folder_path not in task_names_by_folder_path: - instance.set_folder_invalid(True) continue + context_info = info_by_instance_id[instance.id] + context_info.folder_is_valid = True - task_name = instance["task"] - if not task_name: - continue - - if task_name not in task_names_by_folder_path[folder_path]: - instance.set_task_invalid(True) + if ( + not task_name + or task_name in task_names_by_folder_path[folder_path] + ): + context_info.task_is_valid = True + return info_by_instance_id def save_changes(self): """Save changes. Update all changed values.""" diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 4f7caa6e11..9019b05b21 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -1,6 +1,7 @@ import copy import collections from uuid import uuid4 +from typing import Optional from ayon_core.lib.attribute_definitions import ( UnknownDef, @@ -396,6 +397,24 @@ class PublishAttributes: ) +class InstanceContextInfo: + def __init__( + self, + folder_path: Optional[str], + task_name: Optional[str], + folder_is_valid: bool, + task_is_valid: bool, + ): + self.folder_path: Optional[str] = folder_path + self.task_name: Optional[str] = task_name + self.folder_is_valid: bool = folder_is_valid + self.task_is_valid: bool = task_is_valid + + @property + def is_valid(self) -> bool: + return self.folder_is_valid and self.task_is_valid + + class CreatedInstance: """Instance entity with data that will be stored to workfile. @@ -528,9 +547,6 @@ class CreatedInstance: if not self._data.get("instance_id"): self._data["instance_id"] = str(uuid4()) - self._folder_is_valid = self.has_set_folder - self._task_is_valid = self.has_set_task - def __str__(self): return ( " bool: + """Get context data that are promised to be set by creator. + + Returns: + bool: Has context that won't bo validated. Artist can't change + value when set to True. + + """ + return self._transient_data.get("has_promised_context", False) + def data_to_store(self): """Collect data that contain json parsable types. @@ -826,46 +853,3 @@ class CreatedInstance: obj.publish_attributes.deserialize_attributes(publish_attributes) return obj - - # Context validation related methods/properties - @property - def has_set_folder(self): - """Folder path is set in data.""" - - return "folderPath" in self._data - - @property - def has_set_task(self): - """Task name is set in data.""" - - return "task" in self._data - - @property - def has_valid_context(self): - """Context data are valid for publishing.""" - - return self.has_valid_folder and self.has_valid_task - - @property - def has_valid_folder(self): - """Folder set in context exists in project.""" - - if not self.has_set_folder: - return False - return self._folder_is_valid - - @property - def has_valid_task(self): - """Task set in context exists in project.""" - - if not self.has_set_task: - return False - return self._task_is_valid - - def set_folder_invalid(self, invalid): - # TODO replace with `set_folder_path` - self._folder_is_valid = not invalid - - def set_task_invalid(self, invalid): - # TODO replace with `set_task_name` - self._task_is_valid = not invalid diff --git a/client/ayon_core/pipeline/publish/README.md b/client/ayon_core/pipeline/publish/README.md index ee2124dfd3..954c10494f 100644 --- a/client/ayon_core/pipeline/publish/README.md +++ b/client/ayon_core/pipeline/publish/README.md @@ -1,5 +1,5 @@ # Publish -AYON is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. OpenPype's (new) publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception. +AYON is using `pyblish` for publishing process which is a little bit extented and modified mainly for UI purposes. AYON's (new) publish UI does not allow to enable/disable instances or plugins that can be done during creation part. Also does support actions only for validators after validation exception. ## Exceptions AYON define few specific exceptions that should be used in publish plugins. diff --git a/client/ayon_core/pipeline/publish/__init__.py b/client/ayon_core/pipeline/publish/__init__.py index ab19b6e360..cb181c2f2b 100644 --- a/client/ayon_core/pipeline/publish/__init__.py +++ b/client/ayon_core/pipeline/publish/__init__.py @@ -13,7 +13,6 @@ from .publish_plugins import ( PublishXmlValidationError, KnownPublishError, AYONPyblishPluginMixin, - OpenPypePyblishPluginMixin, OptionalPyblishPluginMixin, RepairAction, @@ -66,7 +65,6 @@ __all__ = ( "PublishXmlValidationError", "KnownPublishError", "AYONPyblishPluginMixin", - "OpenPypePyblishPluginMixin", "OptionalPyblishPluginMixin", "RepairAction", diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 8b82622e4c..dc2eef3bb9 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -379,7 +379,7 @@ def get_plugin_settings(plugin, project_settings, log, category=None): plugin_kind = split_path[-2] # TODO: change after all plugins are moved one level up - if category_from_file in ("ayon_core", "openpype"): + if category_from_file == "ayon_core": category_from_file = "core" try: diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 6b1984d92b..1eca8df7cb 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -165,9 +165,6 @@ class AYONPyblishPluginMixin: return self.get_attr_values_from_data_for_plugin(self.__class__, data) -OpenPypePyblishPluginMixin = AYONPyblishPluginMixin - - class OptionalPyblishPluginMixin(AYONPyblishPluginMixin): """Prepare mixin for optional plugins. diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index 29d4659393..d8f42ea60a 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -25,13 +25,7 @@ def create_custom_tempdir(project_name, anatomy=None): """ env_tmpdir = os.getenv("AYON_TMPDIR") if not env_tmpdir: - env_tmpdir = os.getenv("OPENPYPE_TMPDIR") - if not env_tmpdir: - return - print( - "DEPRECATION WARNING: Used 'OPENPYPE_TMPDIR' environment" - " variable. Please use 'AYON_TMPDIR' instead." - ) + return custom_tempdir = None if "{" in env_tmpdir: diff --git a/client/ayon_core/plugins/publish/collect_addons.py b/client/ayon_core/plugins/publish/collect_addons.py index 9bba9978ab..661cf9cb31 100644 --- a/client/ayon_core/plugins/publish/collect_addons.py +++ b/client/ayon_core/plugins/publish/collect_addons.py @@ -15,5 +15,3 @@ class CollectAddons(pyblish.api.ContextPlugin): manager = AddonsManager() context.data["ayonAddonsManager"] = manager context.data["ayonAddons"] = manager.addons_by_name - # Backwards compatibility - remove - context.data["openPypeModules"] = manager.addons_by_name diff --git a/client/ayon_core/plugins/publish/collect_context_entities.py b/client/ayon_core/plugins/publish/collect_context_entities.py index c8d25bc3e6..4de83f0d53 100644 --- a/client/ayon_core/plugins/publish/collect_context_entities.py +++ b/client/ayon_core/plugins/publish/collect_context_entities.py @@ -53,8 +53,9 @@ class CollectContextEntities(pyblish.api.ContextPlugin): context.data["folderEntity"] = folder_entity context.data["taskEntity"] = task_entity - - folder_attributes = folder_entity["attrib"] + context_attributes = ( + task_entity["attrib"] if task_entity else folder_entity["attrib"] + ) # Task type task_type = None @@ -63,12 +64,12 @@ class CollectContextEntities(pyblish.api.ContextPlugin): context.data["taskType"] = task_type - frame_start = folder_attributes.get("frameStart") + frame_start = context_attributes.get("frameStart") if frame_start is None: frame_start = 1 self.log.warning("Missing frame start. Defaulting to 1.") - frame_end = folder_attributes.get("frameEnd") + frame_end = context_attributes.get("frameEnd") if frame_end is None: frame_end = 2 self.log.warning("Missing frame end. Defaulting to 2.") @@ -76,8 +77,8 @@ class CollectContextEntities(pyblish.api.ContextPlugin): context.data["frameStart"] = frame_start context.data["frameEnd"] = frame_end - handle_start = folder_attributes.get("handleStart") or 0 - handle_end = folder_attributes.get("handleEnd") or 0 + handle_start = context_attributes.get("handleStart") or 0 + handle_end = context_attributes.get("handleEnd") or 0 context.data["handleStart"] = int(handle_start) context.data["handleEnd"] = int(handle_end) @@ -87,7 +88,7 @@ class CollectContextEntities(pyblish.api.ContextPlugin): context.data["frameStartHandle"] = frame_start_h context.data["frameEndHandle"] = frame_end_h - context.data["fps"] = folder_attributes["fps"] + context.data["fps"] = context_attributes["fps"] def _get_folder_entity(self, project_name, folder_path): if not folder_path: diff --git a/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py b/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py index b9fe97b80b..f8311f7dfb 100644 --- a/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py +++ b/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py @@ -7,7 +7,7 @@ class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): """Converts collected input representations to input versions. Any data in `instance.data["inputRepresentations"]` gets converted into - `instance.data["inputVersions"]` as supported in OpenPype v3. + `instance.data["inputVersions"]` as supported in OpenPype. """ # This is a ContextPlugin because then we can query the database only once diff --git a/client/ayon_core/plugins/publish/collect_rendered_files.py b/client/ayon_core/plugins/publish/collect_rendered_files.py index 8a60e7619d..42ba096d14 100644 --- a/client/ayon_core/plugins/publish/collect_rendered_files.py +++ b/client/ayon_core/plugins/publish/collect_rendered_files.py @@ -138,10 +138,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): def process(self, context): self._context = context - publish_data_paths = ( - os.environ.get("AYON_PUBLISH_DATA") - or os.environ.get("OPENPYPE_PUBLISH_DATA") - ) + publish_data_paths = os.environ.get("AYON_PUBLISH_DATA") if not publish_data_paths: raise KnownPublishError("Missing `AYON_PUBLISH_DATA`") diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index dff82f3a80..2007240d3d 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -198,8 +198,8 @@ class ExtractBurnin(publish.Extractor): ) if not burnins_per_repres: self.log.debug( - "Skipped instance. No representations found matching a burnin " - "definition in: %s", burnin_defs + "Skipped instance. No representations found matching a burnin" + " definition in: %s", burnin_defs ) return @@ -424,6 +424,7 @@ class ExtractBurnin(publish.Extractor): Returns: dict[str, Any]: Burnin options. + """ # Prepare burnin options burnin_options = copy.deepcopy(self.default_options) @@ -761,6 +762,7 @@ class ExtractBurnin(publish.Extractor): Returns: list[dict[str, Any]]: Contain all valid output definitions. + """ filtered_burnin_defs = [] diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index c1cf50333c..64c73adbd5 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -106,10 +106,19 @@ class ExtractOTIOReview(publish.Extractor): media_metadata = otio_media.metadata # get from media reference metadata source - if media_metadata.get("openpype.source.width"): - width = int(media_metadata.get("openpype.source.width")) - if media_metadata.get("openpype.source.height"): - height = int(media_metadata.get("openpype.source.height")) + # TODO 'openpype' prefix should be removed (added 24/09/03) + # NOTE it looks like it is set only in hiero integration + for key in {"ayon.source.width", "openpype.source.width"}: + value = media_metadata.get(key) + if value is not None: + width = int(value) + break + + for key in {"ayon.source.height", "openpype.source.height"}: + value = media_metadata.get(key) + if value is not None: + height = int(value) + break # compare and reset if width != self.to_width: diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 4390b00754..06b451bfbe 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -95,7 +95,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ] # Supported extensions - image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga"] + image_exts = ["exr", "jpg", "jpeg", "png", "dpx", "tga", "tiff", "tif"] video_exts = ["mov", "mp4"] supported_exts = image_exts + video_exts diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 03ea66f418..acdc5276f7 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -238,7 +238,7 @@ def add_representation(instance, name, class CollectUSDLayerContributions(pyblish.api.InstancePlugin, - publish.OpenPypePyblishPluginMixin): + publish.AYONPyblishPluginMixin): """Collect the USD Layer Contributions and create dependent instances. Our contributions go to the layer diff --git a/client/ayon_core/resources/__init__.py b/client/ayon_core/resources/__init__.py index 2a98cc1968..ea8bf7ca6c 100644 --- a/client/ayon_core/resources/__init__.py +++ b/client/ayon_core/resources/__init__.py @@ -70,19 +70,3 @@ def get_ayon_splash_filepath(staging=None): else: splash_file_name = "AYON_splash.png" return get_resource("icons", splash_file_name) - - -def get_openpype_production_icon_filepath(): - return get_ayon_production_icon_filepath() - - -def get_openpype_staging_icon_filepath(): - return get_ayon_staging_icon_filepath() - - -def get_openpype_icon_filepath(staging=None): - return get_ayon_icon_filepath(staging) - - -def get_openpype_splash_filepath(staging=None): - return get_ayon_splash_filepath(staging) diff --git a/client/ayon_core/scripts/slates/slate_base/items.py b/client/ayon_core/scripts/slates/slate_base/items.py index 6d19fc6a0c..ec3358ed5e 100644 --- a/client/ayon_core/scripts/slates/slate_base/items.py +++ b/client/ayon_core/scripts/slates/slate_base/items.py @@ -486,11 +486,11 @@ class TableField(BaseItem): line = self.ellide_text break - for idx, char in enumerate(_word): + for char_index, char in enumerate(_word): _line = line + char + self.ellide_text _line_width = font.getsize(_line)[0] if _line_width > max_width: - if idx == 0: + if char_index == 0: line = _line break line = line + char diff --git a/client/ayon_core/settings/local_settings.md b/client/ayon_core/settings/local_settings.md deleted file mode 100644 index fbb5cf3df1..0000000000 --- a/client/ayon_core/settings/local_settings.md +++ /dev/null @@ -1,79 +0,0 @@ -# Structure of local settings -- local settings do not have any validation schemas right now this should help to see what is stored to local settings and how it works -- they are stored by identifier site_id which should be unified identifier of workstation -- all keys may and may not available on load -- contain main categories: `general`, `applications`, `projects` - -## Categories -### General -- ATM contain only label of site -```json -{ - "general": { - "site_label": "MySite" - } -} -``` - -### Applications -- modifications of application executables -- output should match application groups and variants -```json -{ - "applications": { - "": { - "": { - "executable": "/my/path/to/nuke_12_2" - } - } - } -} -``` - -### Projects -- project specific modifications -- default project is stored under constant key defined in `pype.settings.contants` -```json -{ - "projects": { - "": { - "active_site": "", - "remote_site": "", - "roots": { - "": { - "": "" - } - } - } - } -} -``` - -## Final document -```json -{ - "_id": "", - "site_id": "", - "general": { - "site_label": "MySite" - }, - "applications": { - "": { - "": { - "executable": "" - } - } - }, - "projects": { - "": { - "active_site": "", - "remote_site": "", - "roots": { - "": { - "": "" - } - } - } - } -} -``` diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 8578522c79..10aa918d08 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -1472,14 +1472,6 @@ CreateNextPageOverlay { 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/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 362fa38882..ad566eb354 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -322,6 +322,12 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ) -> Dict[str, Union[CreatedInstance, None]]: pass + @abstractmethod + def get_instances_context_info( + self, instance_ids: Optional[Iterable[str]] = None + ): + pass + @abstractmethod def get_existing_product_names(self, folder_path: str) -> List[str]: pass diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 257b45de08..fe1545f219 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -190,6 +190,9 @@ class PublisherController( def get_instances_by_id(self, instance_ids=None): return self._create_model.get_instances_by_id(instance_ids) + def get_instances_context_info(self, instance_ids=None): + return self._create_model.get_instances_context_info(instance_ids) + def get_convertor_items(self): return self._create_model.get_convertor_items() diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 9fe114f4bd..dcd2ce4acc 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -306,6 +306,14 @@ class CreateModel: for instance_id in instance_ids } + def get_instances_context_info( + self, instance_ids: Optional[Iterable[str]] = None + ): + instances = self.get_instances_by_id(instance_ids).values() + return self._create_context.get_instances_context_info( + instances + ) + def get_convertor_items(self) -> Dict[str, ConvertorItem]: return self._create_context.convertor_items_by_id diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index d67252e302..c0e27d9c60 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -217,20 +217,22 @@ class InstanceGroupWidget(BaseGroupWidget): def update_icons(self, group_icons): self._group_icons = group_icons - def update_instance_values(self): + def update_instance_values(self, context_info_by_id): """Trigger update on instance widgets.""" - for widget in self._widgets_by_id.values(): - widget.update_instance_values() + for instance_id, widget in self._widgets_by_id.items(): + widget.update_instance_values(context_info_by_id[instance_id]) - def update_instances(self, instances): + def update_instances(self, instances, context_info_by_id): """Update instances for the group. Args: - instances(list): List of instances in + instances (list[CreatedInstance]): List of instances in CreateContext. - """ + context_info_by_id (Dict[str, InstanceContextInfo]): Instance + context info by instance id. + """ # Store instances by id and by product name instances_by_id = {} instances_by_product_name = collections.defaultdict(list) @@ -249,13 +251,14 @@ class InstanceGroupWidget(BaseGroupWidget): widget_idx = 1 for product_names in sorted_product_names: for instance in instances_by_product_name[product_names]: + context_info = context_info_by_id[instance.id] if instance.id in self._widgets_by_id: widget = self._widgets_by_id[instance.id] - widget.update_instance(instance) + widget.update_instance(instance, context_info) else: group_icon = self._group_icons[instance.creator_identifier] widget = InstanceCardWidget( - instance, group_icon, self + instance, context_info, group_icon, self ) widget.selected.connect(self._on_widget_selection) widget.active_changed.connect(self._on_active_changed) @@ -388,7 +391,7 @@ class ConvertorItemCardWidget(CardWidget): self._icon_widget = icon_widget self._label_widget = label_widget - def update_instance_values(self): + def update_instance_values(self, context_info): pass @@ -397,7 +400,7 @@ class InstanceCardWidget(CardWidget): active_changed = QtCore.Signal(str, bool) - def __init__(self, instance, group_icon, parent): + def __init__(self, instance, context_info, group_icon, parent): super().__init__(parent) self._id = instance.id @@ -458,7 +461,7 @@ class InstanceCardWidget(CardWidget): self._active_checkbox = active_checkbox self._expand_btn = expand_btn - self.update_instance_values() + self.update_instance_values(context_info) def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) @@ -480,13 +483,13 @@ class InstanceCardWidget(CardWidget): if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) - def update_instance(self, instance): + def update_instance(self, instance, context_info): """Update instance object and update UI.""" self.instance = instance - self.update_instance_values() + self.update_instance_values(context_info) - def _validate_context(self): - valid = self.instance.has_valid_context + def _validate_context(self, context_info): + valid = context_info.is_valid self._icon_widget.setVisible(valid) self._context_warning.setVisible(not valid) @@ -519,11 +522,11 @@ class InstanceCardWidget(CardWidget): QtCore.Qt.NoTextInteraction ) - def update_instance_values(self): + def update_instance_values(self, context_info): """Update instance data""" self._update_product_name() self.set_active(self.instance["active"]) - self._validate_context() + self._validate_context(context_info) def _set_expanded(self, expanded=None): if expanded is None: @@ -694,6 +697,8 @@ class InstanceCardView(AbstractInstanceView): self._update_convertor_items_group() + context_info_by_id = self._controller.get_instances_context_info() + # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) identifiers_by_group = collections.defaultdict(set) @@ -747,7 +752,7 @@ class InstanceCardView(AbstractInstanceView): widget_idx += 1 group_widget.update_instances( - instances_by_group[group_name] + instances_by_group[group_name], context_info_by_id ) group_widget.set_active_toggle_enabled( self._active_toggle_enabled @@ -814,8 +819,9 @@ class InstanceCardView(AbstractInstanceView): def refresh_instance_states(self): """Trigger update of instances on group widgets.""" + context_info_by_id = self._controller.get_instances_context_info() for widget in self._widgets_by_group.values(): - widget.update_instance_values() + widget.update_instance_values(context_info_by_id) def _on_active_changed(self, group_name, instance_id, value): group_widget = self._widgets_by_group[group_name] diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index 930d6bb88c..ab9f2db52c 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -115,7 +115,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): active_changed = QtCore.Signal(str, bool) double_clicked = QtCore.Signal() - def __init__(self, instance, parent): + def __init__(self, instance, context_info, parent): super().__init__(parent) self.instance = instance @@ -151,7 +151,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): self._has_valid_context = None - self._set_valid_property(instance.has_valid_context) + self._set_valid_property(context_info.is_valid) def mouseDoubleClickEvent(self, event): widget = self.childAt(event.pos()) @@ -188,12 +188,12 @@ class InstanceListItemWidget(QtWidgets.QWidget): if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) - def update_instance(self, instance): + def update_instance(self, instance, context_info): """Update instance object.""" self.instance = instance - self.update_instance_values() + self.update_instance_values(context_info) - def update_instance_values(self): + def update_instance_values(self, context_info): """Update instance data propagated to widgets.""" # Check product name label = self.instance.label @@ -202,7 +202,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): # Check active state self.set_active(self.instance["active"]) # Check valid states - self._set_valid_property(self.instance.has_valid_context) + self._set_valid_property(context_info.is_valid) def _on_active_change(self): new_value = self._active_checkbox.isChecked() @@ -583,6 +583,8 @@ class InstanceListView(AbstractInstanceView): self._update_convertor_items_group() + context_info_by_id = self._controller.get_instances_context_info() + # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() @@ -643,13 +645,15 @@ class InstanceListView(AbstractInstanceView): elif activity != instance["active"]: activity = -1 + context_info = context_info_by_id[instance_id] + self._group_by_instance_id[instance_id] = group_name # Remove instance id from `to_remove` if already exists and # trigger update of widget if instance_id in to_remove: to_remove.remove(instance_id) widget = self._widgets_by_id[instance_id] - widget.update_instance(instance) + widget.update_instance(instance, context_info) continue # Create new item and store it as new @@ -695,7 +699,8 @@ class InstanceListView(AbstractInstanceView): group_item.appendRows(new_items) for item, instance in new_items_with_instance: - if not instance.has_valid_context: + context_info = context_info_by_id[instance.id] + if not context_info.is_valid: expand_groups.add(group_name) item_index = self._instance_model.index( item.row(), @@ -704,7 +709,7 @@ class InstanceListView(AbstractInstanceView): ) proxy_index = self._proxy_model.mapFromSource(item_index) widget = InstanceListItemWidget( - instance, self._instance_view + instance, context_info, self._instance_view ) widget.set_active_toggle_enabled( self._active_toggle_enabled @@ -870,8 +875,10 @@ class InstanceListView(AbstractInstanceView): def refresh_instance_states(self): """Trigger update of all instances.""" - for widget in self._widgets_by_id.values(): - widget.update_instance_values() + context_info_by_id = self._controller.get_instances_context_info() + for instance_id, widget in self._widgets_by_id.items(): + context_info = context_info_by_id[instance_id] + widget.update_instance_values(context_info) def _on_active_changed(self, changed_instance_id, new_value): selected_instance_ids, _, _ = self.get_selected_items() diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index 52a45d0881..d00edb9883 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -387,7 +387,7 @@ class OverviewWidget(QtWidgets.QFrame): Returns: list[str]: Selected legacy convertor identifiers. - Example: ['io.openpype.creators.houdini.legacy'] + Example: ['io.ayon.creators.houdini.legacy'] """ _, _, convertor_identifiers = self.get_selected_items() diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 1f782ddc67..83a2d9e6c1 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1182,6 +1182,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget): invalid_tasks = False folder_paths = [] for instance in self._current_instances: + # Ignore instances that have promised context + if instance.has_promised_context: + continue + new_variant_value = instance.get("variant") new_folder_path = instance.get("folderPath") new_task_name = instance.get("task") @@ -1206,7 +1210,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget): except TaskNotSetError: invalid_tasks = True - instance.set_task_invalid(True) product_names.add(instance["productName"]) continue @@ -1216,11 +1219,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if folder_path is not None: instance["folderPath"] = folder_path - instance.set_folder_invalid(False) if task_name is not None: instance["task"] = task_name or None - instance.set_task_invalid(False) instance["productName"] = new_product_name @@ -1306,7 +1307,13 @@ class GlobalAttrsWidget(QtWidgets.QWidget): editable = False folder_task_combinations = [] + context_editable = None for instance in instances: + if not instance.has_promised_context: + context_editable = True + elif context_editable is None: + context_editable = False + # NOTE I'm not sure how this can even happen? if instance.creator_identifier is None: editable = False @@ -1319,6 +1326,11 @@ class GlobalAttrsWidget(QtWidgets.QWidget): folder_task_combinations.append((folder_path, task_name)) product_names.add(instance.get("productName") or self.unknown_value) + if not editable: + context_editable = False + elif context_editable is None: + context_editable = True + self.variant_input.set_value(variants) # Set context of folder widget @@ -1329,8 +1341,21 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self.product_value_widget.set_value(product_names) self.variant_input.setEnabled(editable) - self.folder_value_widget.setEnabled(editable) - self.task_value_widget.setEnabled(editable) + self.folder_value_widget.setEnabled(context_editable) + self.task_value_widget.setEnabled(context_editable) + + if not editable: + folder_tooltip = "Select instances to change folder path." + task_tooltip = "Select instances to change task name." + elif not context_editable: + folder_tooltip = "Folder path is defined by Create plugin." + task_tooltip = "Task is defined by Create plugin." + else: + folder_tooltip = "Change folder path of selected instances." + task_tooltip = "Change task of selected instances." + + self.folder_value_widget.setToolTip(folder_tooltip) + self.task_value_widget.setToolTip(task_tooltip) class CreatorAttrsWidget(QtWidgets.QWidget): @@ -1339,7 +1364,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): Attributes are defined on creator so are dynamic. Their look and type is based on attribute definitions that are defined in `~/ayon_core/lib/attribute_definitions.py` and their widget - representation in `~/openpype/tools/attribute_defs/*`. + representation in `~/ayon_core/tools/attribute_defs/*`. Widgets are disabled if context of instance is not valid. @@ -1768,9 +1793,16 @@ class ProductAttributesWidget(QtWidgets.QWidget): self.bottom_separator = bottom_separator def _on_instance_context_changed(self): + instance_ids = { + instance.id + for instance in self._current_instances + } + context_info_by_id = self._controller.get_instances_context_info( + instance_ids + ) all_valid = True - for instance in self._current_instances: - if not instance.has_valid_context: + for instance_id, context_info in context_info_by_id.items(): + if not context_info.is_valid: all_valid = False break @@ -1795,9 +1827,17 @@ class ProductAttributesWidget(QtWidgets.QWidget): convertor_identifiers(List[str]): Identifiers of convert items. """ + instance_ids = { + instance.id + for instance in instances + } + context_info_by_id = self._controller.get_instances_context_info( + instance_ids + ) + all_valid = True - for instance in instances: - if not instance.has_valid_context: + for context_info in context_info_by_id.values(): + if not context_info.is_valid: all_valid = False break diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 0c6087b41d..a8ca605ecb 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -913,12 +913,18 @@ class PublisherWindow(QtWidgets.QDialog): self._set_footer_enabled(True) return + active_instances_by_id = { + instance.id: instance + for instance in self._controller.get_instances() + if instance["active"] + } + context_info_by_id = self._controller.get_instances_context_info( + active_instances_by_id.keys() + ) all_valid = None - for instance in self._controller.get_instances(): - if not instance["active"]: - continue - - if not instance.has_valid_context: + for instance_id, instance in active_instances_by_id.items(): + context_info = context_info_by_id[instance_id] + if not context_info.is_valid: all_valid = False break diff --git a/client/ayon_core/tools/pyblish_pype/util.py b/client/ayon_core/tools/pyblish_pype/util.py index 09a370c6e4..d24b07a409 100644 --- a/client/ayon_core/tools/pyblish_pype/util.py +++ b/client/ayon_core/tools/pyblish_pype/util.py @@ -135,7 +135,6 @@ class OrderGroups: def env_variable_to_bool(env_key, default=False): """Boolean based on environment variable value.""" - # TODO: move to pype lib value = os.environ.get(env_key) if value is not None: value = value.lower() diff --git a/client/ayon_core/tools/tray/ui/addons_manager.py b/client/ayon_core/tools/tray/ui/addons_manager.py index 3fe4bb8dd8..2e6f0c0aae 100644 --- a/client/ayon_core/tools/tray/ui/addons_manager.py +++ b/client/ayon_core/tools/tray/ui/addons_manager.py @@ -237,11 +237,8 @@ class TrayAddonsManager(AddonsManager): webserver_url = self.webserver_url statics_url = f"{webserver_url}/res" + # Deprecated # TODO stop using these env variables # - function 'get_tray_server_url' should be used instead os.environ[self.webserver_url_env] = webserver_url os.environ["AYON_STATICS_SERVER"] = statics_url - - # Deprecated - os.environ["OPENPYPE_WEBSERVER_URL"] = webserver_url - os.environ["OPENPYPE_STATICS_SERVER"] = statics_url diff --git a/client/ayon_core/tools/utils/__init__.py b/client/ayon_core/tools/utils/__init__.py index 3e265c7692..4714e76ea3 100644 --- a/client/ayon_core/tools/utils/__init__.py +++ b/client/ayon_core/tools/utils/__init__.py @@ -38,7 +38,6 @@ from .lib import ( qt_app_context, get_qt_app, get_ayon_qt_app, - get_openpype_qt_app, get_qt_icon, ) @@ -122,7 +121,6 @@ __all__ = ( "qt_app_context", "get_qt_app", "get_ayon_qt_app", - "get_openpype_qt_app", "get_qt_icon", "RecursiveSortFilterProxyModel", diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index 8689a97451..200e281664 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -196,10 +196,6 @@ def get_ayon_qt_app(): return app -def get_openpype_qt_app(): - return get_ayon_qt_app() - - def iter_model_rows(model, column=0, include_root=False): """Iterate over all row indices in a model""" indexes_queue = collections.deque()