Merge branch 'chore/extract_burning_missing_space' of https://github.com/BigRoy/ayon-core into chore/extract_burning_missing_space

This commit is contained in:
Roy Nieterau 2024-09-18 00:52:00 +02:00
commit 0d3f9dfdd5
44 changed files with 343 additions and 760 deletions

View file

@ -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",
)

View file

@ -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)

View file

@ -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")

View file

@ -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",

View file

@ -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")

View file

@ -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)

View file

@ -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

View file

@ -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",
)

View file

@ -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",
)

View file

@ -1 +0,0 @@
from ayon_core.addon.click_wrap import *

View file

@ -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",
)

View file

@ -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",

View file

@ -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()

View file

@ -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."""

View file

@ -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 (
"<CreatedInstance {product[name]}"
@ -699,6 +715,17 @@ class CreatedInstance:
def publish_attributes(self):
return self._data["publish_attributes"]
@property
def has_promised_context(self) -> 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

View file

@ -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.

View file

@ -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",

View file

@ -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:

View file

@ -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.

View file

@ -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:

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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`")

View file

@ -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 = []

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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": {
"<app group>": {
"<app name>": {
"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": {
"<project name>": {
"active_site": "<name of active site>",
"remote_site": "<name of remote site>",
"roots": {
"<site name>": {
"<root name>": "<root dir path>"
}
}
}
}
}
```
## Final document
```json
{
"_id": "<ObjectId(...)>",
"site_id": "<site id>",
"general": {
"site_label": "MySite"
},
"applications": {
"<app group>": {
"<app name>": {
"executable": "<path to app executable>"
}
}
},
"projects": {
"<project name>": {
"active_site": "<name of active site>",
"remote_site": "<name of remote site>",
"roots": {
"<site name>": {
"<root name>": "<root dir path>"
}
}
}
}
}
```

View file

@ -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;
}

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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<CreatedInstance>): 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]

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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",

View file

@ -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()