mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 12:54:40 +01:00
Merge branch 'develop' into feature/OP-1188_better-representation-model
This commit is contained in:
commit
67b052c691
67 changed files with 2264 additions and 2427 deletions
|
|
@ -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",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -94,4 +94,4 @@ class GlobalHostDataHook(PreLaunchHook):
|
|||
task_entity = get_task_by_name(
|
||||
project_name, folder_entity["id"], task_name
|
||||
)
|
||||
self.data["task_entity"] = task_entity
|
||||
self.data["task_entity"] = task_entity
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -577,7 +578,7 @@ class BoolDef(AbstractAttrDef):
|
|||
return self.default
|
||||
|
||||
|
||||
class FileDefItem(object):
|
||||
class FileDefItem:
|
||||
def __init__(
|
||||
self, directory, filenames, frames=None, template=None
|
||||
):
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import logging
|
|||
import weakref
|
||||
from uuid import uuid4
|
||||
|
||||
from .python_2_comp import WeakMethod
|
||||
from .python_module_tools import is_func_signature_supported
|
||||
|
||||
|
||||
|
|
@ -18,7 +17,7 @@ class MissingEventSystem(Exception):
|
|||
|
||||
def _get_func_ref(func):
|
||||
if inspect.ismethod(func):
|
||||
return WeakMethod(func)
|
||||
return weakref.WeakMethod(func)
|
||||
return weakref.ref(func)
|
||||
|
||||
|
||||
|
|
@ -123,7 +122,7 @@ class weakref_partial:
|
|||
)
|
||||
|
||||
|
||||
class EventCallback(object):
|
||||
class EventCallback:
|
||||
"""Callback registered to a topic.
|
||||
|
||||
The callback function is registered to a topic. Topic is a string which
|
||||
|
|
@ -380,8 +379,7 @@ class EventCallback(object):
|
|||
self._partial_func = None
|
||||
|
||||
|
||||
# Inherit from 'object' for Python 2 hosts
|
||||
class Event(object):
|
||||
class Event:
|
||||
"""Base event object.
|
||||
|
||||
Can be used for any event because is not specific. Only required argument
|
||||
|
|
@ -488,7 +486,7 @@ class Event(object):
|
|||
return obj
|
||||
|
||||
|
||||
class EventSystem(object):
|
||||
class EventSystem:
|
||||
"""Encapsulate event handling into an object.
|
||||
|
||||
System wraps registered callbacks and triggered events into single object,
|
||||
|
|
|
|||
|
|
@ -108,6 +108,20 @@ def run_subprocess(*args, **kwargs):
|
|||
| getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
)
|
||||
|
||||
# Escape parentheses for bash
|
||||
if (
|
||||
kwargs.get("shell") is True
|
||||
and len(args) == 1
|
||||
and isinstance(args[0], str)
|
||||
and os.getenv("SHELL") in ("/bin/bash", "/bin/sh")
|
||||
):
|
||||
new_arg = (
|
||||
args[0]
|
||||
.replace("(", "\\(")
|
||||
.replace(")", "\\)")
|
||||
)
|
||||
args = (new_arg, )
|
||||
|
||||
# Get environents from kwarg or use current process environments if were
|
||||
# not passed.
|
||||
env = kwargs.get("env") or os.environ
|
||||
|
|
@ -221,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.
|
||||
|
||||
|
|
@ -327,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.
|
||||
|
||||
|
|
@ -342,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]
|
||||
|
|
@ -400,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)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class DuplicateDestinationError(ValueError):
|
|||
"""
|
||||
|
||||
|
||||
class FileTransaction(object):
|
||||
class FileTransaction:
|
||||
"""File transaction with rollback options.
|
||||
|
||||
The file transaction is a three-step process.
|
||||
|
|
|
|||
|
|
@ -3,27 +3,11 @@
|
|||
import os
|
||||
import json
|
||||
import platform
|
||||
import configparser
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
# disable lru cache in Python 2
|
||||
try:
|
||||
from functools import lru_cache
|
||||
except ImportError:
|
||||
def lru_cache(maxsize):
|
||||
def max_size(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
value = func(*args, **kwargs)
|
||||
return value
|
||||
return wrapper
|
||||
return max_size
|
||||
|
||||
# ConfigParser was renamed in python3 to configparser
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
from functools import lru_cache
|
||||
|
||||
import appdirs
|
||||
import ayon_api
|
||||
|
|
@ -600,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
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class TemplateUnsolved(Exception):
|
|||
)
|
||||
|
||||
|
||||
class StringTemplate(object):
|
||||
class StringTemplate:
|
||||
"""String that can be formatted."""
|
||||
def __init__(self, template):
|
||||
if not isinstance(template, str):
|
||||
|
|
@ -410,7 +410,7 @@ class TemplatePartResult:
|
|||
self._invalid_types[key] = type(value)
|
||||
|
||||
|
||||
class FormatObject(object):
|
||||
class FormatObject:
|
||||
"""Object that can be used for formatting.
|
||||
|
||||
This is base that is valid for to be used in 'StringTemplate' value.
|
||||
|
|
@ -503,7 +503,7 @@ class FormattingPart:
|
|||
# ensure key is properly formed [({})] properly closed.
|
||||
if not self.validate_key_is_matched(key):
|
||||
result.add_missing_key(key)
|
||||
result.add_output(self.template)
|
||||
result.add_output(self.template)
|
||||
return result
|
||||
|
||||
# check if key expects subdictionary keys (e.g. project[name])
|
||||
|
|
|
|||
|
|
@ -1,44 +1,17 @@
|
|||
# Deprecated file
|
||||
# - the file container 'WeakMethod' implementation for Python 2 which is not
|
||||
# needed anymore.
|
||||
import warnings
|
||||
import weakref
|
||||
|
||||
|
||||
WeakMethod = getattr(weakref, "WeakMethod", None)
|
||||
WeakMethod = weakref.WeakMethod
|
||||
|
||||
if WeakMethod is None:
|
||||
class _WeakCallable:
|
||||
def __init__(self, obj, func):
|
||||
self.im_self = obj
|
||||
self.im_func = func
|
||||
|
||||
def __call__(self, *args, **kws):
|
||||
if self.im_self is None:
|
||||
return self.im_func(*args, **kws)
|
||||
else:
|
||||
return self.im_func(self.im_self, *args, **kws)
|
||||
|
||||
|
||||
class WeakMethod:
|
||||
""" Wraps a function or, more importantly, a bound method in
|
||||
a way that allows a bound method's object to be GCed, while
|
||||
providing the same interface as a normal weak reference. """
|
||||
|
||||
def __init__(self, fn):
|
||||
try:
|
||||
self._obj = weakref.ref(fn.im_self)
|
||||
self._meth = fn.im_func
|
||||
except AttributeError:
|
||||
# It's not a bound method
|
||||
self._obj = None
|
||||
self._meth = fn
|
||||
|
||||
def __call__(self):
|
||||
if self._dead():
|
||||
return None
|
||||
return _WeakCallable(self._getobj(), self._meth)
|
||||
|
||||
def _dead(self):
|
||||
return self._obj is not None and self._obj() is None
|
||||
|
||||
def _getobj(self):
|
||||
if self._obj is None:
|
||||
return None
|
||||
return self._obj()
|
||||
warnings.warn(
|
||||
(
|
||||
"'ayon_core.lib.python_2_comp' is deprecated."
|
||||
"Please use 'weakref.WeakMethod'."
|
||||
),
|
||||
DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,43 +5,30 @@ import importlib
|
|||
import inspect
|
||||
import logging
|
||||
|
||||
import six
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_filepath(filepath, module_name=None):
|
||||
"""Import python file as python module.
|
||||
|
||||
Python 2 and Python 3 compatibility.
|
||||
|
||||
Args:
|
||||
filepath(str): Path to python file.
|
||||
module_name(str): Name of loaded module. Only for Python 3. By default
|
||||
filepath (str): Path to python file.
|
||||
module_name (str): Name of loaded module. Only for Python 3. By default
|
||||
is filled with filename of filepath.
|
||||
|
||||
"""
|
||||
if module_name is None:
|
||||
module_name = os.path.splitext(os.path.basename(filepath))[0]
|
||||
|
||||
# Make sure it is not 'unicode' in Python 2
|
||||
module_name = str(module_name)
|
||||
|
||||
# Prepare module object where content of file will be parsed
|
||||
module = types.ModuleType(module_name)
|
||||
module.__file__ = filepath
|
||||
|
||||
if six.PY3:
|
||||
# Use loader so module has full specs
|
||||
module_loader = importlib.machinery.SourceFileLoader(
|
||||
module_name, filepath
|
||||
)
|
||||
module_loader.exec_module(module)
|
||||
else:
|
||||
# Execute module code and store content to module
|
||||
with open(filepath) as _stream:
|
||||
# Execute content and store it to module object
|
||||
six.exec_(_stream.read(), module.__dict__)
|
||||
|
||||
# Use loader so module has full specs
|
||||
module_loader = importlib.machinery.SourceFileLoader(
|
||||
module_name, filepath
|
||||
)
|
||||
module_loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
|
|
@ -139,35 +126,31 @@ def classes_from_module(superclass, module):
|
|||
return classes
|
||||
|
||||
|
||||
def _import_module_from_dirpath_py2(dirpath, module_name, dst_module_name):
|
||||
"""Import passed dirpath as python module using `imp`."""
|
||||
def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None):
|
||||
"""Import passed directory as a python module.
|
||||
|
||||
Imported module can be assigned as a child attribute of already loaded
|
||||
module from `sys.modules` if has support of `setattr`. That is not default
|
||||
behavior of python modules so parent module must be a custom module with
|
||||
that ability.
|
||||
|
||||
It is not possible to reimport already cached module. If you need to
|
||||
reimport module you have to remove it from caches manually.
|
||||
|
||||
Args:
|
||||
dirpath (str): Parent directory path of loaded folder.
|
||||
folder_name (str): Folder name which should be imported inside passed
|
||||
directory.
|
||||
dst_module_name (str): Parent module name under which can be loaded
|
||||
module added.
|
||||
|
||||
"""
|
||||
# Import passed dirpath as python module
|
||||
if dst_module_name:
|
||||
full_module_name = "{}.{}".format(dst_module_name, module_name)
|
||||
full_module_name = "{}.{}".format(dst_module_name, folder_name)
|
||||
dst_module = sys.modules[dst_module_name]
|
||||
else:
|
||||
full_module_name = module_name
|
||||
dst_module = None
|
||||
|
||||
if full_module_name in sys.modules:
|
||||
return sys.modules[full_module_name]
|
||||
|
||||
import imp
|
||||
|
||||
fp, pathname, description = imp.find_module(module_name, [dirpath])
|
||||
module = imp.load_module(full_module_name, fp, pathname, description)
|
||||
if dst_module is not None:
|
||||
setattr(dst_module, module_name, module)
|
||||
|
||||
return module
|
||||
|
||||
|
||||
def _import_module_from_dirpath_py3(dirpath, module_name, dst_module_name):
|
||||
"""Import passed dirpath as python module using Python 3 modules."""
|
||||
if dst_module_name:
|
||||
full_module_name = "{}.{}".format(dst_module_name, module_name)
|
||||
dst_module = sys.modules[dst_module_name]
|
||||
else:
|
||||
full_module_name = module_name
|
||||
full_module_name = folder_name
|
||||
dst_module = None
|
||||
|
||||
# Skip import if is already imported
|
||||
|
|
@ -191,7 +174,7 @@ def _import_module_from_dirpath_py3(dirpath, module_name, dst_module_name):
|
|||
# Store module to destination module and `sys.modules`
|
||||
# WARNING this mus be done before module execution
|
||||
if dst_module is not None:
|
||||
setattr(dst_module, module_name, module)
|
||||
setattr(dst_module, folder_name, module)
|
||||
|
||||
sys.modules[full_module_name] = module
|
||||
|
||||
|
|
@ -201,37 +184,6 @@ def _import_module_from_dirpath_py3(dirpath, module_name, dst_module_name):
|
|||
return module
|
||||
|
||||
|
||||
def import_module_from_dirpath(dirpath, folder_name, dst_module_name=None):
|
||||
"""Import passed directory as a python module.
|
||||
|
||||
Python 2 and 3 compatible.
|
||||
|
||||
Imported module can be assigned as a child attribute of already loaded
|
||||
module from `sys.modules` if has support of `setattr`. That is not default
|
||||
behavior of python modules so parent module must be a custom module with
|
||||
that ability.
|
||||
|
||||
It is not possible to reimport already cached module. If you need to
|
||||
reimport module you have to remove it from caches manually.
|
||||
|
||||
Args:
|
||||
dirpath(str): Parent directory path of loaded folder.
|
||||
folder_name(str): Folder name which should be imported inside passed
|
||||
directory.
|
||||
dst_module_name(str): Parent module name under which can be loaded
|
||||
module added.
|
||||
"""
|
||||
if six.PY3:
|
||||
module = _import_module_from_dirpath_py3(
|
||||
dirpath, folder_name, dst_module_name
|
||||
)
|
||||
else:
|
||||
module = _import_module_from_dirpath_py2(
|
||||
dirpath, folder_name, dst_module_name
|
||||
)
|
||||
return module
|
||||
|
||||
|
||||
def is_func_signature_supported(func, *args, **kwargs):
|
||||
"""Check if a function signature supports passed args and kwargs.
|
||||
|
||||
|
|
@ -275,25 +227,12 @@ def is_func_signature_supported(func, *args, **kwargs):
|
|||
|
||||
Returns:
|
||||
bool: Function can pass in arguments.
|
||||
|
||||
"""
|
||||
|
||||
if hasattr(inspect, "signature"):
|
||||
# Python 3 using 'Signature' object where we try to bind arg
|
||||
# or kwarg. Using signature is recommended approach based on
|
||||
# documentation.
|
||||
sig = inspect.signature(func)
|
||||
try:
|
||||
sig.bind(*args, **kwargs)
|
||||
return True
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
else:
|
||||
# In Python 2 'signature' is not available so 'getcallargs' is used
|
||||
# - 'getcallargs' is marked as deprecated since Python 3.0
|
||||
try:
|
||||
inspect.getcallargs(func, *args, **kwargs)
|
||||
return True
|
||||
except TypeError:
|
||||
pass
|
||||
sig = inspect.signature(func)
|
||||
try:
|
||||
sig.bind(*args, **kwargs)
|
||||
return True
|
||||
except TypeError:
|
||||
pass
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -1 +0,0 @@
|
|||
from ayon_core.addon.click_wrap import *
|
||||
|
|
@ -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",
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
@ -170,7 +168,6 @@ __all__ = (
|
|||
"PublishXmlValidationError",
|
||||
"KnownPublishError",
|
||||
"AYONPyblishPluginMixin",
|
||||
"OpenPypePyblishPluginMixin",
|
||||
"OptionalPyblishPluginMixin",
|
||||
|
||||
# --- Actions ---
|
||||
|
|
@ -189,7 +186,6 @@ __all__ = (
|
|||
|
||||
# --- Process context ---
|
||||
"install_ayon_plugins",
|
||||
"install_openpype_plugins",
|
||||
"install_host",
|
||||
"uninstall_host",
|
||||
"is_installed",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -4,21 +4,41 @@ from .constants import (
|
|||
PRE_CREATE_THUMBNAIL_KEY,
|
||||
DEFAULT_VARIANT_VALUE,
|
||||
)
|
||||
|
||||
from .exceptions import (
|
||||
UnavailableSharedData,
|
||||
ImmutableKeyError,
|
||||
HostMissRequiredMethod,
|
||||
ConvertorsOperationFailed,
|
||||
ConvertorsFindFailed,
|
||||
ConvertorsConversionFailed,
|
||||
CreatorError,
|
||||
CreatorsCreateFailed,
|
||||
CreatorsCollectionFailed,
|
||||
CreatorsSaveFailed,
|
||||
CreatorsRemoveFailed,
|
||||
CreatorsOperationFailed,
|
||||
TaskNotSetError,
|
||||
TemplateFillError,
|
||||
)
|
||||
from .structures import (
|
||||
CreatedInstance,
|
||||
ConvertorItem,
|
||||
AttributeValues,
|
||||
CreatorAttributeValues,
|
||||
PublishAttributeValues,
|
||||
PublishAttributes,
|
||||
)
|
||||
from .utils import (
|
||||
get_last_versions_for_instances,
|
||||
get_next_versions_for_instances,
|
||||
)
|
||||
|
||||
from .product_name import (
|
||||
TaskNotSetError,
|
||||
get_product_name,
|
||||
get_product_name_template,
|
||||
)
|
||||
|
||||
from .creator_plugins import (
|
||||
CreatorError,
|
||||
|
||||
BaseCreator,
|
||||
Creator,
|
||||
AutoCreator,
|
||||
|
|
@ -36,10 +56,7 @@ from .creator_plugins import (
|
|||
cache_and_get_instances,
|
||||
)
|
||||
|
||||
from .context import (
|
||||
CreatedInstance,
|
||||
CreateContext
|
||||
)
|
||||
from .context import CreateContext
|
||||
|
||||
from .legacy_create import (
|
||||
LegacyCreator,
|
||||
|
|
@ -53,10 +70,31 @@ __all__ = (
|
|||
"PRE_CREATE_THUMBNAIL_KEY",
|
||||
"DEFAULT_VARIANT_VALUE",
|
||||
|
||||
"UnavailableSharedData",
|
||||
"ImmutableKeyError",
|
||||
"HostMissRequiredMethod",
|
||||
"ConvertorsOperationFailed",
|
||||
"ConvertorsFindFailed",
|
||||
"ConvertorsConversionFailed",
|
||||
"CreatorError",
|
||||
"CreatorsCreateFailed",
|
||||
"CreatorsCollectionFailed",
|
||||
"CreatorsSaveFailed",
|
||||
"CreatorsRemoveFailed",
|
||||
"CreatorsOperationFailed",
|
||||
"TaskNotSetError",
|
||||
"TemplateFillError",
|
||||
|
||||
"CreatedInstance",
|
||||
"ConvertorItem",
|
||||
"AttributeValues",
|
||||
"CreatorAttributeValues",
|
||||
"PublishAttributeValues",
|
||||
"PublishAttributes",
|
||||
|
||||
"get_last_versions_for_instances",
|
||||
"get_next_versions_for_instances",
|
||||
|
||||
"TaskNotSetError",
|
||||
"get_product_name",
|
||||
"get_product_name_template",
|
||||
|
||||
|
|
@ -78,7 +116,6 @@ __all__ = (
|
|||
|
||||
"cache_and_get_instances",
|
||||
|
||||
"CreatedInstance",
|
||||
"CreateContext",
|
||||
|
||||
"LegacyCreator",
|
||||
|
|
|
|||
313
client/ayon_core/pipeline/create/changes.py
Normal file
313
client/ayon_core/pipeline/create/changes.py
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
import copy
|
||||
|
||||
_EMPTY_VALUE = object()
|
||||
|
||||
|
||||
class TrackChangesItem:
|
||||
"""Helper object to track changes in data.
|
||||
|
||||
Has access to full old and new data and will create deep copy of them,
|
||||
so it is not needed to create copy before passed in.
|
||||
|
||||
Can work as a dictionary if old or new value is a dictionary. In
|
||||
that case received object is another object of 'TrackChangesItem'.
|
||||
|
||||
Goal is to be able to get old or new value as was or only changed values
|
||||
or get information about removed/changed keys, and all of that on
|
||||
any "dictionary level".
|
||||
|
||||
```
|
||||
# Example of possible usages
|
||||
>>> old_value = {
|
||||
... "key_1": "value_1",
|
||||
... "key_2": {
|
||||
... "key_sub_1": 1,
|
||||
... "key_sub_2": {
|
||||
... "enabled": True
|
||||
... }
|
||||
... },
|
||||
... "key_3": "value_2"
|
||||
... }
|
||||
>>> new_value = {
|
||||
... "key_1": "value_1",
|
||||
... "key_2": {
|
||||
... "key_sub_2": {
|
||||
... "enabled": False
|
||||
... },
|
||||
... "key_sub_3": 3
|
||||
... },
|
||||
... "key_3": "value_3"
|
||||
... }
|
||||
|
||||
>>> changes = TrackChangesItem(old_value, new_value)
|
||||
>>> changes.changed
|
||||
True
|
||||
|
||||
>>> changes["key_2"]["key_sub_1"].new_value is None
|
||||
True
|
||||
|
||||
>>> list(sorted(changes.changed_keys))
|
||||
['key_2', 'key_3']
|
||||
|
||||
>>> changes["key_2"]["key_sub_2"]["enabled"].changed
|
||||
True
|
||||
|
||||
>>> changes["key_2"].removed_keys
|
||||
{'key_sub_1'}
|
||||
|
||||
>>> list(sorted(changes["key_2"].available_keys))
|
||||
['key_sub_1', 'key_sub_2', 'key_sub_3']
|
||||
|
||||
>>> changes.new_value == new_value
|
||||
True
|
||||
|
||||
# Get only changed values
|
||||
only_changed_new_values = {
|
||||
key: changes[key].new_value
|
||||
for key in changes.changed_keys
|
||||
}
|
||||
```
|
||||
|
||||
Args:
|
||||
old_value (Any): Old value.
|
||||
new_value (Any): New value.
|
||||
"""
|
||||
|
||||
def __init__(self, old_value, new_value):
|
||||
self._changed = old_value != new_value
|
||||
# Resolve if value is '_EMPTY_VALUE' after comparison of the values
|
||||
if old_value is _EMPTY_VALUE:
|
||||
old_value = None
|
||||
if new_value is _EMPTY_VALUE:
|
||||
new_value = None
|
||||
self._old_value = copy.deepcopy(old_value)
|
||||
self._new_value = copy.deepcopy(new_value)
|
||||
|
||||
self._old_is_dict = isinstance(old_value, dict)
|
||||
self._new_is_dict = isinstance(new_value, dict)
|
||||
|
||||
self._old_keys = None
|
||||
self._new_keys = None
|
||||
self._available_keys = None
|
||||
self._removed_keys = None
|
||||
|
||||
self._changed_keys = None
|
||||
|
||||
self._sub_items = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Getter looks into subitems if object is dictionary."""
|
||||
|
||||
if self._sub_items is None:
|
||||
self._prepare_sub_items()
|
||||
return self._sub_items[key]
|
||||
|
||||
def __bool__(self):
|
||||
"""Boolean of object is if old and new value are the same."""
|
||||
|
||||
return self._changed
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Try to get sub item."""
|
||||
|
||||
if self._sub_items is None:
|
||||
self._prepare_sub_items()
|
||||
return self._sub_items.get(key, default)
|
||||
|
||||
@property
|
||||
def old_value(self):
|
||||
"""Get copy of old value.
|
||||
|
||||
Returns:
|
||||
Any: Whatever old value was.
|
||||
"""
|
||||
|
||||
return copy.deepcopy(self._old_value)
|
||||
|
||||
@property
|
||||
def new_value(self):
|
||||
"""Get copy of new value.
|
||||
|
||||
Returns:
|
||||
Any: Whatever new value was.
|
||||
"""
|
||||
|
||||
return copy.deepcopy(self._new_value)
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
"""Value changed.
|
||||
|
||||
Returns:
|
||||
bool: If data changed.
|
||||
"""
|
||||
|
||||
return self._changed
|
||||
|
||||
@property
|
||||
def is_dict(self):
|
||||
"""Object can be used as dictionary.
|
||||
|
||||
Returns:
|
||||
bool: When can be used that way.
|
||||
"""
|
||||
|
||||
return self._old_is_dict or self._new_is_dict
|
||||
|
||||
@property
|
||||
def changes(self):
|
||||
"""Get changes in raw data.
|
||||
|
||||
This method should be used only if 'is_dict' value is 'True'.
|
||||
|
||||
Returns:
|
||||
Dict[str, Tuple[Any, Any]]: Changes are by key in tuple
|
||||
(<old value>, <new value>). If 'is_dict' is 'False' then
|
||||
output is always empty dictionary.
|
||||
"""
|
||||
|
||||
output = {}
|
||||
if not self.is_dict:
|
||||
return output
|
||||
|
||||
old_value = self.old_value
|
||||
new_value = self.new_value
|
||||
for key in self.changed_keys:
|
||||
_old = None
|
||||
_new = None
|
||||
if self._old_is_dict:
|
||||
_old = old_value.get(key)
|
||||
if self._new_is_dict:
|
||||
_new = new_value.get(key)
|
||||
output[key] = (_old, _new)
|
||||
return output
|
||||
|
||||
# Methods/properties that can be used when 'is_dict' is 'True'
|
||||
@property
|
||||
def old_keys(self):
|
||||
"""Keys from old value.
|
||||
|
||||
Empty set is returned if old value is not a dict.
|
||||
|
||||
Returns:
|
||||
Set[str]: Keys from old value.
|
||||
"""
|
||||
|
||||
if self._old_keys is None:
|
||||
self._prepare_keys()
|
||||
return set(self._old_keys)
|
||||
|
||||
@property
|
||||
def new_keys(self):
|
||||
"""Keys from new value.
|
||||
|
||||
Empty set is returned if old value is not a dict.
|
||||
|
||||
Returns:
|
||||
Set[str]: Keys from new value.
|
||||
"""
|
||||
|
||||
if self._new_keys is None:
|
||||
self._prepare_keys()
|
||||
return set(self._new_keys)
|
||||
|
||||
@property
|
||||
def changed_keys(self):
|
||||
"""Keys that has changed from old to new value.
|
||||
|
||||
Empty set is returned if both old and new value are not a dict.
|
||||
|
||||
Returns:
|
||||
Set[str]: Keys of changed keys.
|
||||
"""
|
||||
|
||||
if self._changed_keys is None:
|
||||
self._prepare_sub_items()
|
||||
return set(self._changed_keys)
|
||||
|
||||
@property
|
||||
def available_keys(self):
|
||||
"""All keys that are available in old and new value.
|
||||
|
||||
Empty set is returned if both old and new value are not a dict.
|
||||
Output is Union of 'old_keys' and 'new_keys'.
|
||||
|
||||
Returns:
|
||||
Set[str]: All keys from old and new value.
|
||||
"""
|
||||
|
||||
if self._available_keys is None:
|
||||
self._prepare_keys()
|
||||
return set(self._available_keys)
|
||||
|
||||
@property
|
||||
def removed_keys(self):
|
||||
"""Key that are not available in new value but were in old value.
|
||||
|
||||
Returns:
|
||||
Set[str]: All removed keys.
|
||||
"""
|
||||
|
||||
if self._removed_keys is None:
|
||||
self._prepare_sub_items()
|
||||
return set(self._removed_keys)
|
||||
|
||||
def _prepare_keys(self):
|
||||
old_keys = set()
|
||||
new_keys = set()
|
||||
if self._old_is_dict and self._new_is_dict:
|
||||
old_keys = set(self._old_value.keys())
|
||||
new_keys = set(self._new_value.keys())
|
||||
|
||||
elif self._old_is_dict:
|
||||
old_keys = set(self._old_value.keys())
|
||||
|
||||
elif self._new_is_dict:
|
||||
new_keys = set(self._new_value.keys())
|
||||
|
||||
self._old_keys = old_keys
|
||||
self._new_keys = new_keys
|
||||
self._available_keys = old_keys | new_keys
|
||||
self._removed_keys = old_keys - new_keys
|
||||
|
||||
def _prepare_sub_items(self):
|
||||
sub_items = {}
|
||||
changed_keys = set()
|
||||
|
||||
old_keys = self.old_keys
|
||||
new_keys = self.new_keys
|
||||
new_value = self.new_value
|
||||
old_value = self.old_value
|
||||
if self._old_is_dict and self._new_is_dict:
|
||||
for key in self.available_keys:
|
||||
item = TrackChangesItem(
|
||||
old_value.get(key), new_value.get(key)
|
||||
)
|
||||
sub_items[key] = item
|
||||
if item.changed or key not in old_keys or key not in new_keys:
|
||||
changed_keys.add(key)
|
||||
|
||||
elif self._old_is_dict:
|
||||
old_keys = set(old_value.keys())
|
||||
available_keys = set(old_keys)
|
||||
changed_keys = set(available_keys)
|
||||
for key in available_keys:
|
||||
# NOTE Use '_EMPTY_VALUE' because old value could be 'None'
|
||||
# which would result in "unchanged" item
|
||||
sub_items[key] = TrackChangesItem(
|
||||
old_value.get(key), _EMPTY_VALUE
|
||||
)
|
||||
|
||||
elif self._new_is_dict:
|
||||
new_keys = set(new_value.keys())
|
||||
available_keys = set(new_keys)
|
||||
changed_keys = set(available_keys)
|
||||
for key in available_keys:
|
||||
# NOTE Use '_EMPTY_VALUE' because new value could be 'None'
|
||||
# which would result in "unchanged" item
|
||||
sub_items[key] = TrackChangesItem(
|
||||
_EMPTY_VALUE, new_value.get(key)
|
||||
)
|
||||
|
||||
self._sub_items = sub_items
|
||||
self._changed_keys = changed_keys
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -26,16 +26,6 @@ if TYPE_CHECKING:
|
|||
from .context import CreateContext, CreatedInstance, UpdateData # noqa: F401
|
||||
|
||||
|
||||
class CreatorError(Exception):
|
||||
"""Should be raised when creator failed because of known issue.
|
||||
|
||||
Message of error should be user readable.
|
||||
"""
|
||||
|
||||
def __init__(self, message):
|
||||
super(CreatorError, self).__init__(message)
|
||||
|
||||
|
||||
class ProductConvertorPlugin(ABC):
|
||||
"""Helper for conversion of instances created using legacy creators.
|
||||
|
||||
|
|
@ -654,7 +644,7 @@ class Creator(BaseCreator):
|
|||
cls._get_default_variant_wrap,
|
||||
cls._set_default_variant_wrap
|
||||
)
|
||||
super(Creator, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def show_order(self):
|
||||
|
|
|
|||
127
client/ayon_core/pipeline/create/exceptions.py
Normal file
127
client/ayon_core/pipeline/create/exceptions.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import os
|
||||
import inspect
|
||||
|
||||
|
||||
class UnavailableSharedData(Exception):
|
||||
"""Shared data are not available at the moment when are accessed."""
|
||||
pass
|
||||
|
||||
|
||||
class ImmutableKeyError(TypeError):
|
||||
"""Accessed key is immutable so does not allow changes or removals."""
|
||||
|
||||
def __init__(self, key, msg=None):
|
||||
self.immutable_key = key
|
||||
if not msg:
|
||||
msg = "Key \"{}\" is immutable and does not allow changes.".format(
|
||||
key
|
||||
)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class HostMissRequiredMethod(Exception):
|
||||
"""Host does not have implemented required functions for creation."""
|
||||
|
||||
def __init__(self, host, missing_methods):
|
||||
self.missing_methods = missing_methods
|
||||
self.host = host
|
||||
joined_methods = ", ".join(
|
||||
['"{}"'.format(name) for name in missing_methods]
|
||||
)
|
||||
dirpath = os.path.dirname(
|
||||
os.path.normpath(inspect.getsourcefile(host))
|
||||
)
|
||||
dirpath_parts = dirpath.split(os.path.sep)
|
||||
host_name = dirpath_parts.pop(-1)
|
||||
if host_name == "api":
|
||||
host_name = dirpath_parts.pop(-1)
|
||||
|
||||
msg = "Host \"{}\" does not have implemented method/s {}".format(
|
||||
host_name, joined_methods
|
||||
)
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class ConvertorsOperationFailed(Exception):
|
||||
def __init__(self, msg, failed_info):
|
||||
super().__init__(msg)
|
||||
self.failed_info = failed_info
|
||||
|
||||
|
||||
class ConvertorsFindFailed(ConvertorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to find incompatible products"
|
||||
super().__init__(msg, failed_info)
|
||||
|
||||
|
||||
class ConvertorsConversionFailed(ConvertorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to convert incompatible products"
|
||||
super().__init__(msg, failed_info)
|
||||
|
||||
|
||||
class CreatorError(Exception):
|
||||
"""Should be raised when creator failed because of known issue.
|
||||
|
||||
Message of error should be artist friendly.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CreatorsOperationFailed(Exception):
|
||||
"""Raised when a creator process crashes in 'CreateContext'.
|
||||
|
||||
The exception contains information about the creator and error. The data
|
||||
are prepared using 'prepare_failed_creator_operation_info' and can be
|
||||
serialized using json.
|
||||
|
||||
Usage is for UI purposes which may not have access to exceptions directly
|
||||
and would not have ability to catch exceptions 'per creator'.
|
||||
|
||||
Args:
|
||||
msg (str): General error message.
|
||||
failed_info (list[dict[str, Any]]): List of failed creators with
|
||||
exception message and optionally formatted traceback.
|
||||
"""
|
||||
|
||||
def __init__(self, msg, failed_info):
|
||||
super().__init__(msg)
|
||||
self.failed_info = failed_info
|
||||
|
||||
|
||||
class CreatorsCollectionFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to collect instances"
|
||||
super().__init__(msg, failed_info)
|
||||
|
||||
|
||||
class CreatorsSaveFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed update instance changes"
|
||||
super().__init__(msg, failed_info)
|
||||
|
||||
|
||||
class CreatorsRemoveFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to remove instances"
|
||||
super().__init__(msg, failed_info)
|
||||
|
||||
|
||||
class CreatorsCreateFailed(CreatorsOperationFailed):
|
||||
def __init__(self, failed_info):
|
||||
msg = "Failed to create instances"
|
||||
super().__init__(msg, failed_info)
|
||||
|
||||
|
||||
class TaskNotSetError(KeyError):
|
||||
def __init__(self, msg=None):
|
||||
if not msg:
|
||||
msg = "Creator's product name template requires task name."
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class TemplateFillError(Exception):
|
||||
def __init__(self, msg=None):
|
||||
if not msg:
|
||||
msg = "Creator's product name template is missing key value."
|
||||
super().__init__(msg)
|
||||
|
|
@ -14,7 +14,7 @@ from ayon_core.pipeline.constants import AVALON_INSTANCE_ID
|
|||
from .product_name import get_product_name
|
||||
|
||||
|
||||
class LegacyCreator(object):
|
||||
class LegacyCreator:
|
||||
"""Determine how assets are created"""
|
||||
label = None
|
||||
product_type = None
|
||||
|
|
|
|||
|
|
@ -3,20 +3,7 @@ from ayon_core.lib import StringTemplate, filter_profiles, prepare_template_data
|
|||
from ayon_core.settings import get_project_settings
|
||||
|
||||
from .constants import DEFAULT_PRODUCT_TEMPLATE
|
||||
|
||||
|
||||
class TaskNotSetError(KeyError):
|
||||
def __init__(self, msg=None):
|
||||
if not msg:
|
||||
msg = "Creator's product name template requires task name."
|
||||
super(TaskNotSetError, self).__init__(msg)
|
||||
|
||||
|
||||
class TemplateFillError(Exception):
|
||||
def __init__(self, msg=None):
|
||||
if not msg:
|
||||
msg = "Creator's product name template is missing key value."
|
||||
super(TemplateFillError, self).__init__(msg)
|
||||
from .exceptions import TaskNotSetError, TemplateFillError
|
||||
|
||||
|
||||
def get_product_name_template(
|
||||
|
|
|
|||
855
client/ayon_core/pipeline/create/structures.py
Normal file
855
client/ayon_core/pipeline/create/structures.py
Normal file
|
|
@ -0,0 +1,855 @@
|
|||
import copy
|
||||
import collections
|
||||
from uuid import uuid4
|
||||
from typing import Optional
|
||||
|
||||
from ayon_core.lib.attribute_definitions import (
|
||||
UnknownDef,
|
||||
serialize_attr_defs,
|
||||
deserialize_attr_defs,
|
||||
)
|
||||
from ayon_core.pipeline import (
|
||||
AYON_INSTANCE_ID,
|
||||
AVALON_INSTANCE_ID,
|
||||
)
|
||||
|
||||
from .exceptions import ImmutableKeyError
|
||||
from .changes import TrackChangesItem
|
||||
|
||||
|
||||
class ConvertorItem:
|
||||
"""Item representing convertor plugin.
|
||||
|
||||
Args:
|
||||
identifier (str): Identifier of convertor.
|
||||
label (str): Label which will be shown in UI.
|
||||
"""
|
||||
|
||||
def __init__(self, identifier, label):
|
||||
self._id = str(uuid4())
|
||||
self.identifier = identifier
|
||||
self.label = label
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def to_data(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"identifier": self.identifier,
|
||||
"label": self.label
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data):
|
||||
obj = cls(data["identifier"], data["label"])
|
||||
obj._id = data["id"]
|
||||
return obj
|
||||
|
||||
|
||||
class InstanceMember:
|
||||
"""Representation of instance member.
|
||||
|
||||
TODO:
|
||||
Implement and use!
|
||||
"""
|
||||
|
||||
def __init__(self, instance, name):
|
||||
self.instance = instance
|
||||
|
||||
instance.add_members(self)
|
||||
|
||||
self.name = name
|
||||
self._actions = []
|
||||
|
||||
def add_action(self, label, callback):
|
||||
self._actions.append({
|
||||
"label": label,
|
||||
"callback": callback
|
||||
})
|
||||
|
||||
|
||||
class AttributeValues:
|
||||
"""Container which keep values of Attribute definitions.
|
||||
|
||||
Goal is to have one object which hold values of attribute definitions for
|
||||
single instance.
|
||||
|
||||
Has dictionary like methods. Not all of them are allowed all the time.
|
||||
|
||||
Args:
|
||||
attr_defs(AbstractAttrDef): Definitions of value type and properties.
|
||||
values(dict): Values after possible conversion.
|
||||
origin_data(dict): Values loaded from host before conversion.
|
||||
"""
|
||||
|
||||
def __init__(self, attr_defs, values, origin_data=None):
|
||||
if origin_data is None:
|
||||
origin_data = copy.deepcopy(values)
|
||||
self._origin_data = origin_data
|
||||
|
||||
attr_defs_by_key = {
|
||||
attr_def.key: attr_def
|
||||
for attr_def in attr_defs
|
||||
if attr_def.is_value_def
|
||||
}
|
||||
for key, value in values.items():
|
||||
if key not in attr_defs_by_key:
|
||||
new_def = UnknownDef(key, label=key, default=value)
|
||||
attr_defs.append(new_def)
|
||||
attr_defs_by_key[key] = new_def
|
||||
|
||||
self._attr_defs = attr_defs
|
||||
self._attr_defs_by_key = attr_defs_by_key
|
||||
|
||||
self._data = {}
|
||||
for attr_def in attr_defs:
|
||||
value = values.get(attr_def.key)
|
||||
if value is not None:
|
||||
self._data[attr_def.key] = value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self._attr_defs_by_key:
|
||||
raise KeyError("Key \"{}\" was not found.".format(key))
|
||||
|
||||
self.update({key: value})
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key not in self._attr_defs_by_key:
|
||||
return self._data[key]
|
||||
return self._data.get(key, self._attr_defs_by_key[key].default)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._attr_defs_by_key
|
||||
|
||||
def get(self, key, default=None):
|
||||
if key in self._attr_defs_by_key:
|
||||
return self[key]
|
||||
return default
|
||||
|
||||
def keys(self):
|
||||
return self._attr_defs_by_key.keys()
|
||||
|
||||
def values(self):
|
||||
for key in self._attr_defs_by_key.keys():
|
||||
yield self._data.get(key)
|
||||
|
||||
def items(self):
|
||||
for key in self._attr_defs_by_key.keys():
|
||||
yield key, self._data.get(key)
|
||||
|
||||
def update(self, value):
|
||||
changes = {}
|
||||
for _key, _value in dict(value).items():
|
||||
if _key in self._data and self._data.get(_key) == _value:
|
||||
continue
|
||||
self._data[_key] = _value
|
||||
changes[_key] = _value
|
||||
|
||||
def pop(self, key, default=None):
|
||||
value = self._data.pop(key, default)
|
||||
# Remove attribute definition if is 'UnknownDef'
|
||||
# - gives option to get rid of unknown values
|
||||
attr_def = self._attr_defs_by_key.get(key)
|
||||
if isinstance(attr_def, UnknownDef):
|
||||
self._attr_defs_by_key.pop(key)
|
||||
self._attr_defs.remove(attr_def)
|
||||
return value
|
||||
|
||||
def reset_values(self):
|
||||
self._data = {}
|
||||
|
||||
def mark_as_stored(self):
|
||||
self._origin_data = copy.deepcopy(self._data)
|
||||
|
||||
@property
|
||||
def attr_defs(self):
|
||||
"""Pointer to attribute definitions.
|
||||
|
||||
Returns:
|
||||
List[AbstractAttrDef]: Attribute definitions.
|
||||
"""
|
||||
|
||||
return list(self._attr_defs)
|
||||
|
||||
@property
|
||||
def origin_data(self):
|
||||
return copy.deepcopy(self._origin_data)
|
||||
|
||||
def data_to_store(self):
|
||||
"""Create new dictionary with data to store.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Attribute values that should be stored.
|
||||
"""
|
||||
|
||||
output = {}
|
||||
for key in self._data:
|
||||
output[key] = self[key]
|
||||
|
||||
for key, attr_def in self._attr_defs_by_key.items():
|
||||
if key not in output:
|
||||
output[key] = attr_def.default
|
||||
return output
|
||||
|
||||
def get_serialized_attr_defs(self):
|
||||
"""Serialize attribute definitions to json serializable types.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, Any]]: Serialized attribute definitions.
|
||||
"""
|
||||
|
||||
return serialize_attr_defs(self._attr_defs)
|
||||
|
||||
|
||||
class CreatorAttributeValues(AttributeValues):
|
||||
"""Creator specific attribute values of an instance.
|
||||
|
||||
Args:
|
||||
instance (CreatedInstance): Instance for which are values hold.
|
||||
"""
|
||||
|
||||
def __init__(self, instance, *args, **kwargs):
|
||||
self.instance = instance
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class PublishAttributeValues(AttributeValues):
|
||||
"""Publish plugin specific attribute values.
|
||||
|
||||
Values are for single plugin which can be on `CreatedInstance`
|
||||
or context values stored on `CreateContext`.
|
||||
|
||||
Args:
|
||||
publish_attributes(PublishAttributes): Wrapper for multiple publish
|
||||
attributes is used as parent object.
|
||||
"""
|
||||
|
||||
def __init__(self, publish_attributes, *args, **kwargs):
|
||||
self.publish_attributes = publish_attributes
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.publish_attributes.parent
|
||||
|
||||
|
||||
class PublishAttributes:
|
||||
"""Wrapper for publish plugin attribute definitions.
|
||||
|
||||
Cares about handling attribute definitions of multiple publish plugins.
|
||||
Keep information about attribute definitions and their values.
|
||||
|
||||
Args:
|
||||
parent(CreatedInstance, CreateContext): Parent for which will be
|
||||
data stored and from which are data loaded.
|
||||
origin_data(dict): Loaded data by plugin class name.
|
||||
attr_plugins(Union[List[pyblish.api.Plugin], None]): List of publish
|
||||
plugins that may have defined attribute definitions.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, origin_data, attr_plugins=None):
|
||||
self.parent = parent
|
||||
self._origin_data = copy.deepcopy(origin_data)
|
||||
|
||||
attr_plugins = attr_plugins or []
|
||||
self.attr_plugins = attr_plugins
|
||||
|
||||
self._data = copy.deepcopy(origin_data)
|
||||
self._plugin_names_order = []
|
||||
self._missing_plugins = []
|
||||
|
||||
self.set_publish_plugins(attr_plugins)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._data
|
||||
|
||||
def keys(self):
|
||||
return self._data.keys()
|
||||
|
||||
def values(self):
|
||||
return self._data.values()
|
||||
|
||||
def items(self):
|
||||
return self._data.items()
|
||||
|
||||
def pop(self, key, default=None):
|
||||
"""Remove or reset value for plugin.
|
||||
|
||||
Plugin values are reset to defaults if plugin is available but
|
||||
data of plugin which was not found are removed.
|
||||
|
||||
Args:
|
||||
key(str): Plugin name.
|
||||
default: Default value if plugin was not found.
|
||||
"""
|
||||
|
||||
if key not in self._data:
|
||||
return default
|
||||
|
||||
if key in self._missing_plugins:
|
||||
self._missing_plugins.remove(key)
|
||||
removed_item = self._data.pop(key)
|
||||
return removed_item.data_to_store()
|
||||
|
||||
value_item = self._data[key]
|
||||
# Prepare value to return
|
||||
output = value_item.data_to_store()
|
||||
# Reset values
|
||||
value_item.reset_values()
|
||||
return output
|
||||
|
||||
def plugin_names_order(self):
|
||||
"""Plugin names order by their 'order' attribute."""
|
||||
|
||||
for name in self._plugin_names_order:
|
||||
yield name
|
||||
|
||||
def mark_as_stored(self):
|
||||
self._origin_data = copy.deepcopy(self.data_to_store())
|
||||
|
||||
def data_to_store(self):
|
||||
"""Convert attribute values to "data to store"."""
|
||||
|
||||
output = {}
|
||||
for key, attr_value in self._data.items():
|
||||
output[key] = attr_value.data_to_store()
|
||||
return output
|
||||
|
||||
@property
|
||||
def origin_data(self):
|
||||
return copy.deepcopy(self._origin_data)
|
||||
|
||||
def set_publish_plugins(self, attr_plugins):
|
||||
"""Set publish plugins attribute definitions."""
|
||||
|
||||
self._plugin_names_order = []
|
||||
self._missing_plugins = []
|
||||
self.attr_plugins = attr_plugins or []
|
||||
|
||||
origin_data = self._origin_data
|
||||
data = self._data
|
||||
self._data = {}
|
||||
added_keys = set()
|
||||
for plugin in attr_plugins:
|
||||
output = plugin.convert_attribute_values(data)
|
||||
if output is not None:
|
||||
data = output
|
||||
attr_defs = plugin.get_attribute_defs()
|
||||
if not attr_defs:
|
||||
continue
|
||||
|
||||
key = plugin.__name__
|
||||
added_keys.add(key)
|
||||
self._plugin_names_order.append(key)
|
||||
|
||||
value = data.get(key) or {}
|
||||
orig_value = copy.deepcopy(origin_data.get(key) or {})
|
||||
self._data[key] = PublishAttributeValues(
|
||||
self, attr_defs, value, orig_value
|
||||
)
|
||||
|
||||
for key, value in data.items():
|
||||
if key not in added_keys:
|
||||
self._missing_plugins.append(key)
|
||||
self._data[key] = PublishAttributeValues(
|
||||
self, [], value, value
|
||||
)
|
||||
|
||||
def serialize_attributes(self):
|
||||
return {
|
||||
"attr_defs": {
|
||||
plugin_name: attrs_value.get_serialized_attr_defs()
|
||||
for plugin_name, attrs_value in self._data.items()
|
||||
},
|
||||
"plugin_names_order": self._plugin_names_order,
|
||||
"missing_plugins": self._missing_plugins
|
||||
}
|
||||
|
||||
def deserialize_attributes(self, data):
|
||||
self._plugin_names_order = data["plugin_names_order"]
|
||||
self._missing_plugins = data["missing_plugins"]
|
||||
|
||||
attr_defs = deserialize_attr_defs(data["attr_defs"])
|
||||
|
||||
origin_data = self._origin_data
|
||||
data = self._data
|
||||
self._data = {}
|
||||
|
||||
added_keys = set()
|
||||
for plugin_name, attr_defs_data in attr_defs.items():
|
||||
attr_defs = deserialize_attr_defs(attr_defs_data)
|
||||
value = data.get(plugin_name) or {}
|
||||
orig_value = copy.deepcopy(origin_data.get(plugin_name) or {})
|
||||
self._data[plugin_name] = PublishAttributeValues(
|
||||
self, attr_defs, value, orig_value
|
||||
)
|
||||
|
||||
for key, value in data.items():
|
||||
if key not in added_keys:
|
||||
self._missing_plugins.append(key)
|
||||
self._data[key] = PublishAttributeValues(
|
||||
self, [], value, value
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
I think `data` must be required argument containing all minimum information
|
||||
about instance like "folderPath" and "task" and all data used for filling
|
||||
product name as creators may have custom data for product name filling.
|
||||
|
||||
Notes:
|
||||
Object have 2 possible initialization. One using 'creator' object which
|
||||
is recommended for api usage. Second by passing information about
|
||||
creator.
|
||||
|
||||
Args:
|
||||
product_type (str): Product type that will be created.
|
||||
product_name (str): Name of product that will be created.
|
||||
data (Dict[str, Any]): Data used for filling product name or override
|
||||
data from already existing instance.
|
||||
creator (Union[BaseCreator, None]): Creator responsible for instance.
|
||||
creator_identifier (str): Identifier of creator plugin.
|
||||
creator_label (str): Creator plugin label.
|
||||
group_label (str): Default group label from creator plugin.
|
||||
creator_attr_defs (List[AbstractAttrDef]): Attribute definitions from
|
||||
creator.
|
||||
"""
|
||||
|
||||
# Keys that can't be changed or removed from data after loading using
|
||||
# creator.
|
||||
# - 'creator_attributes' and 'publish_attributes' can change values of
|
||||
# their individual children but not on their own
|
||||
__immutable_keys = (
|
||||
"id",
|
||||
"instance_id",
|
||||
"product_type",
|
||||
"creator_identifier",
|
||||
"creator_attributes",
|
||||
"publish_attributes"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
product_type,
|
||||
product_name,
|
||||
data,
|
||||
creator=None,
|
||||
creator_identifier=None,
|
||||
creator_label=None,
|
||||
group_label=None,
|
||||
creator_attr_defs=None,
|
||||
):
|
||||
if creator is not None:
|
||||
creator_identifier = creator.identifier
|
||||
group_label = creator.get_group_label()
|
||||
creator_label = creator.label
|
||||
creator_attr_defs = creator.get_instance_attr_defs()
|
||||
|
||||
self._creator_label = creator_label
|
||||
self._group_label = group_label or creator_identifier
|
||||
|
||||
# Instance members may have actions on them
|
||||
# TODO implement members logic
|
||||
self._members = []
|
||||
|
||||
# Data that can be used for lifetime of object
|
||||
self._transient_data = {}
|
||||
|
||||
# Create a copy of passed data to avoid changing them on the fly
|
||||
data = copy.deepcopy(data or {})
|
||||
|
||||
# Pop dictionary values that will be converted to objects to be able
|
||||
# catch changes
|
||||
orig_creator_attributes = data.pop("creator_attributes", None) or {}
|
||||
orig_publish_attributes = data.pop("publish_attributes", None) or {}
|
||||
|
||||
# Store original value of passed data
|
||||
self._orig_data = copy.deepcopy(data)
|
||||
|
||||
# Pop 'productType' and 'productName' to prevent unexpected changes
|
||||
data.pop("productType", None)
|
||||
data.pop("productName", None)
|
||||
# Backwards compatibility with OpenPype instances
|
||||
data.pop("family", None)
|
||||
data.pop("subset", None)
|
||||
|
||||
asset_name = data.pop("asset", None)
|
||||
if "folderPath" not in data:
|
||||
data["folderPath"] = asset_name
|
||||
|
||||
# QUESTION Does it make sense to have data stored as ordered dict?
|
||||
self._data = collections.OrderedDict()
|
||||
# QUESTION Do we need this "id" information on instance?
|
||||
item_id = data.get("id")
|
||||
# TODO use only 'AYON_INSTANCE_ID' when all hosts support it
|
||||
if item_id not in {AYON_INSTANCE_ID, AVALON_INSTANCE_ID}:
|
||||
item_id = AVALON_INSTANCE_ID
|
||||
self._data["id"] = item_id
|
||||
self._data["productType"] = product_type
|
||||
self._data["productName"] = product_name
|
||||
self._data["active"] = data.get("active", True)
|
||||
self._data["creator_identifier"] = creator_identifier
|
||||
|
||||
# Pop from source data all keys that are defined in `_data` before
|
||||
# this moment and through their values away
|
||||
# - they should be the same and if are not then should not change
|
||||
# already set values
|
||||
for key in self._data.keys():
|
||||
if key in data:
|
||||
data.pop(key)
|
||||
|
||||
self._data["variant"] = self._data.get("variant") or ""
|
||||
# Stored creator specific attribute values
|
||||
# {key: value}
|
||||
creator_values = copy.deepcopy(orig_creator_attributes)
|
||||
|
||||
self._data["creator_attributes"] = CreatorAttributeValues(
|
||||
self,
|
||||
list(creator_attr_defs),
|
||||
creator_values,
|
||||
orig_creator_attributes
|
||||
)
|
||||
|
||||
# Stored publish specific attribute values
|
||||
# {<plugin name>: {key: value}}
|
||||
# - must be set using 'set_publish_plugins'
|
||||
self._data["publish_attributes"] = PublishAttributes(
|
||||
self, orig_publish_attributes, None
|
||||
)
|
||||
if data:
|
||||
self._data.update(data)
|
||||
|
||||
if not self._data.get("instance_id"):
|
||||
self._data["instance_id"] = str(uuid4())
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"<CreatedInstance {product[name]}"
|
||||
" ({product[type]}[{creator_identifier}])> {data}"
|
||||
).format(
|
||||
creator_identifier=self.creator_identifier,
|
||||
product={"name": self.product_name, "type": self.product_type},
|
||||
data=str(self._data)
|
||||
)
|
||||
|
||||
# --- Dictionary like methods ---
|
||||
def __getitem__(self, key):
|
||||
return self._data[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._data
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
# Validate immutable keys
|
||||
if key not in self.__immutable_keys:
|
||||
self._data[key] = value
|
||||
|
||||
elif value != self._data.get(key):
|
||||
# Raise exception if key is immutable and value has changed
|
||||
raise ImmutableKeyError(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._data.get(key, default)
|
||||
|
||||
def pop(self, key, *args, **kwargs):
|
||||
# Raise exception if is trying to pop key which is immutable
|
||||
if key in self.__immutable_keys:
|
||||
raise ImmutableKeyError(key)
|
||||
|
||||
self._data.pop(key, *args, **kwargs)
|
||||
|
||||
def keys(self):
|
||||
return self._data.keys()
|
||||
|
||||
def values(self):
|
||||
return self._data.values()
|
||||
|
||||
def items(self):
|
||||
return self._data.items()
|
||||
# ------
|
||||
|
||||
@property
|
||||
def product_type(self):
|
||||
return self._data["productType"]
|
||||
|
||||
@property
|
||||
def product_name(self):
|
||||
return self._data["productName"]
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
label = self._data.get("label")
|
||||
if not label:
|
||||
label = self.product_name
|
||||
return label
|
||||
|
||||
@property
|
||||
def group_label(self):
|
||||
label = self._data.get("group")
|
||||
if label:
|
||||
return label
|
||||
return self._group_label
|
||||
|
||||
@property
|
||||
def origin_data(self):
|
||||
output = copy.deepcopy(self._orig_data)
|
||||
output["creator_attributes"] = self.creator_attributes.origin_data
|
||||
output["publish_attributes"] = self.publish_attributes.origin_data
|
||||
return output
|
||||
|
||||
@property
|
||||
def creator_identifier(self):
|
||||
return self._data["creator_identifier"]
|
||||
|
||||
@property
|
||||
def creator_label(self):
|
||||
return self._creator_label or self.creator_identifier
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
"""Instance identifier.
|
||||
|
||||
Returns:
|
||||
str: UUID of instance.
|
||||
"""
|
||||
|
||||
return self._data["instance_id"]
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
"""Legacy access to data.
|
||||
|
||||
Access to data is needed to modify values.
|
||||
|
||||
Returns:
|
||||
CreatedInstance: Object can be used as dictionary but with
|
||||
validations of immutable keys.
|
||||
"""
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def transient_data(self):
|
||||
"""Data stored for lifetime of instance object.
|
||||
|
||||
These data are not stored to scene and will be lost on object
|
||||
deletion.
|
||||
|
||||
Can be used to store objects. In some host implementations is not
|
||||
possible to reference to object in scene with some unique identifier
|
||||
(e.g. node in Fusion.). In that case it is handy to store the object
|
||||
here. Should be used that way only if instance data are stored on the
|
||||
node itself.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Dictionary object where you can store data related
|
||||
to instance for lifetime of instance object.
|
||||
"""
|
||||
|
||||
return self._transient_data
|
||||
|
||||
def changes(self):
|
||||
"""Calculate and return changes."""
|
||||
|
||||
return TrackChangesItem(self.origin_data, self.data_to_store())
|
||||
|
||||
def mark_as_stored(self):
|
||||
"""Should be called when instance data are stored.
|
||||
|
||||
Origin data are replaced by current data so changes are cleared.
|
||||
"""
|
||||
|
||||
orig_keys = set(self._orig_data.keys())
|
||||
for key, value in self._data.items():
|
||||
orig_keys.discard(key)
|
||||
if key in ("creator_attributes", "publish_attributes"):
|
||||
continue
|
||||
self._orig_data[key] = copy.deepcopy(value)
|
||||
|
||||
for key in orig_keys:
|
||||
self._orig_data.pop(key)
|
||||
|
||||
self.creator_attributes.mark_as_stored()
|
||||
self.publish_attributes.mark_as_stored()
|
||||
|
||||
@property
|
||||
def creator_attributes(self):
|
||||
return self._data["creator_attributes"]
|
||||
|
||||
@property
|
||||
def creator_attribute_defs(self):
|
||||
"""Attribute definitions defined by creator plugin.
|
||||
|
||||
Returns:
|
||||
List[AbstractAttrDef]: Attribute definitions.
|
||||
"""
|
||||
|
||||
return self.creator_attributes.attr_defs
|
||||
|
||||
@property
|
||||
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.
|
||||
|
||||
It is possible to recreate the instance using these data.
|
||||
|
||||
Todos:
|
||||
We probably don't need OrderedDict. When data are loaded they
|
||||
are not ordered anymore.
|
||||
|
||||
Returns:
|
||||
OrderedDict: Ordered dictionary with instance data.
|
||||
"""
|
||||
|
||||
output = collections.OrderedDict()
|
||||
for key, value in self._data.items():
|
||||
if key in ("creator_attributes", "publish_attributes"):
|
||||
continue
|
||||
output[key] = value
|
||||
|
||||
output["creator_attributes"] = self.creator_attributes.data_to_store()
|
||||
output["publish_attributes"] = self.publish_attributes.data_to_store()
|
||||
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def from_existing(cls, instance_data, creator):
|
||||
"""Convert instance data from workfile to CreatedInstance.
|
||||
|
||||
Args:
|
||||
instance_data (Dict[str, Any]): Data in a structure ready for
|
||||
'CreatedInstance' object.
|
||||
creator (BaseCreator): Creator plugin which is creating the
|
||||
instance of for which the instance belong.
|
||||
"""
|
||||
|
||||
instance_data = copy.deepcopy(instance_data)
|
||||
|
||||
product_type = instance_data.get("productType")
|
||||
if product_type is None:
|
||||
product_type = instance_data.get("family")
|
||||
if product_type is None:
|
||||
product_type = creator.product_type
|
||||
product_name = instance_data.get("productName")
|
||||
if product_name is None:
|
||||
product_name = instance_data.get("subset")
|
||||
|
||||
return cls(
|
||||
product_type, product_name, instance_data, creator
|
||||
)
|
||||
|
||||
def set_publish_plugins(self, attr_plugins):
|
||||
"""Set publish plugins with attribute definitions.
|
||||
|
||||
This method should be called only from 'CreateContext'.
|
||||
|
||||
Args:
|
||||
attr_plugins (List[pyblish.api.Plugin]): Pyblish plugins which
|
||||
inherit from 'AYONPyblishPluginMixin' and may contain
|
||||
attribute definitions.
|
||||
"""
|
||||
|
||||
self.publish_attributes.set_publish_plugins(attr_plugins)
|
||||
|
||||
def add_members(self, members):
|
||||
"""Currently unused method."""
|
||||
|
||||
for member in members:
|
||||
if member not in self._members:
|
||||
self._members.append(member)
|
||||
|
||||
def serialize_for_remote(self):
|
||||
"""Serialize object into data to be possible recreated object.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Serialized data.
|
||||
"""
|
||||
|
||||
creator_attr_defs = self.creator_attributes.get_serialized_attr_defs()
|
||||
publish_attributes = self.publish_attributes.serialize_attributes()
|
||||
return {
|
||||
"data": self.data_to_store(),
|
||||
"orig_data": self.origin_data,
|
||||
"creator_attr_defs": creator_attr_defs,
|
||||
"publish_attributes": publish_attributes,
|
||||
"creator_label": self._creator_label,
|
||||
"group_label": self._group_label,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def deserialize_on_remote(cls, serialized_data):
|
||||
"""Convert instance data to CreatedInstance.
|
||||
|
||||
This is fake instance in remote process e.g. in UI process. The creator
|
||||
is not a full creator and should not be used for calling methods when
|
||||
instance is created from this method (matters on implementation).
|
||||
|
||||
Args:
|
||||
serialized_data (Dict[str, Any]): Serialized data for remote
|
||||
recreating. Should contain 'data' and 'orig_data'.
|
||||
"""
|
||||
|
||||
instance_data = copy.deepcopy(serialized_data["data"])
|
||||
creator_identifier = instance_data["creator_identifier"]
|
||||
|
||||
product_type = instance_data["productType"]
|
||||
product_name = instance_data.get("productName", None)
|
||||
|
||||
creator_label = serialized_data["creator_label"]
|
||||
group_label = serialized_data["group_label"]
|
||||
creator_attr_defs = deserialize_attr_defs(
|
||||
serialized_data["creator_attr_defs"]
|
||||
)
|
||||
publish_attributes = serialized_data["publish_attributes"]
|
||||
|
||||
obj = cls(
|
||||
product_type,
|
||||
product_name,
|
||||
instance_data,
|
||||
creator_identifier=creator_identifier,
|
||||
creator_label=creator_label,
|
||||
group_label=group_label,
|
||||
creator_attr_defs=creator_attr_defs
|
||||
)
|
||||
obj._orig_data = serialized_data["orig_data"]
|
||||
obj.publish_attributes.deserialize_attributes(publish_attributes)
|
||||
|
||||
return obj
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -373,7 +373,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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -859,7 +859,7 @@ class AbstractTemplateBuilder(ABC):
|
|||
"Settings\\Profiles"
|
||||
).format(host_name.title()))
|
||||
|
||||
# Try fill path with environments and anatomy roots
|
||||
# Try to fill path with environments and anatomy roots
|
||||
anatomy = Anatomy(project_name)
|
||||
fill_data = {
|
||||
key: value
|
||||
|
|
@ -872,9 +872,7 @@ class AbstractTemplateBuilder(ABC):
|
|||
"code": anatomy.project_code,
|
||||
}
|
||||
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if result.solved:
|
||||
path = result.normalized()
|
||||
path = self.resolve_template_path(path, fill_data)
|
||||
|
||||
if path and os.path.exists(path):
|
||||
self.log.info("Found template at: '{}'".format(path))
|
||||
|
|
@ -914,6 +912,27 @@ class AbstractTemplateBuilder(ABC):
|
|||
"create_first_version": create_first_version
|
||||
}
|
||||
|
||||
def resolve_template_path(self, path, fill_data) -> str:
|
||||
"""Resolve the template path.
|
||||
|
||||
By default, this does nothing except returning the path directly.
|
||||
|
||||
This can be overridden in host integrations to perform additional
|
||||
resolving over the template. Like, `hou.text.expandString` in Houdini.
|
||||
|
||||
Arguments:
|
||||
path (str): The input path.
|
||||
fill_data (dict[str, str]): Data to use for template formatting.
|
||||
|
||||
Returns:
|
||||
str: The resolved path.
|
||||
|
||||
"""
|
||||
result = StringTemplate.format_template(path, fill_data)
|
||||
if result.solved:
|
||||
path = result.normalized()
|
||||
return path
|
||||
|
||||
def emit_event(self, topic, data=None, source=None) -> Event:
|
||||
return self._event_system.emit(topic, data, source)
|
||||
|
||||
|
|
@ -1519,9 +1538,10 @@ class PlaceholderLoadMixin(object):
|
|||
if "asset" in placeholder.data:
|
||||
return []
|
||||
|
||||
representation_name = placeholder.data["representation"]
|
||||
if not representation_name:
|
||||
return []
|
||||
representation_names = None
|
||||
representation_name: str = placeholder.data["representation"]
|
||||
if representation_name:
|
||||
representation_names = [representation_name]
|
||||
|
||||
project_name = self.builder.project_name
|
||||
current_folder_entity = self.builder.current_folder_entity
|
||||
|
|
@ -1578,7 +1598,7 @@ class PlaceholderLoadMixin(object):
|
|||
)
|
||||
return list(get_representations(
|
||||
project_name,
|
||||
representation_names={representation_name},
|
||||
representation_names=representation_names,
|
||||
version_ids=version_ids
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -217,9 +217,8 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
joined_paths = ", ".join(
|
||||
["\"{}\"".format(path) for path in not_found_task_paths]
|
||||
)
|
||||
self.log.warning((
|
||||
"Not found task entities with paths \"{}\"."
|
||||
).format(joined_paths))
|
||||
self.log.warning(
|
||||
f"Not found task entities with paths {joined_paths}.")
|
||||
|
||||
def fill_latest_versions(self, context, project_name):
|
||||
"""Try to find latest version for each instance's product name.
|
||||
|
|
@ -321,7 +320,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
|
|||
use_context_version = instance.data["followWorkfileVersion"]
|
||||
|
||||
if use_context_version:
|
||||
version_number = context.data("version")
|
||||
version_number = context.data.get("version")
|
||||
|
||||
# Even if 'follow_workfile_version' is enabled, it may not be set
|
||||
# because workfile version was not collected to 'context.data'
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -113,4 +114,4 @@ class CollectContextEntities(pyblish.api.ContextPlugin):
|
|||
"Task '{}' was not found in project '{}'.".format(
|
||||
task_path, project_name)
|
||||
)
|
||||
return task_entity
|
||||
return task_entity
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`")
|
||||
|
||||
|
|
|
|||
|
|
@ -47,8 +47,9 @@ class CollectSceneVersion(pyblish.api.ContextPlugin):
|
|||
return
|
||||
|
||||
if not context.data.get('currentFile'):
|
||||
raise KnownPublishError("Cannot get current workfile path. "
|
||||
"Make sure your scene is saved.")
|
||||
self.log.error("Cannot get current workfile path. "
|
||||
"Make sure your scene is saved.")
|
||||
return
|
||||
|
||||
filename = os.path.basename(context.data.get('currentFile'))
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
hosts = ["resolve", "hiero", "flame"]
|
||||
|
||||
# plugin default attributes
|
||||
temp_file_head = "tempFile."
|
||||
to_width = 1280
|
||||
to_height = 720
|
||||
output_ext = ".jpg"
|
||||
|
|
@ -62,6 +61,9 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
make_sequence_collection
|
||||
)
|
||||
|
||||
# TODO refactore from using instance variable
|
||||
self.temp_file_head = self._get_folder_name_based_prefix(instance)
|
||||
|
||||
# TODO: convert resulting image sequence to mp4
|
||||
|
||||
# get otio clip and other time info from instance clip
|
||||
|
|
@ -104,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:
|
||||
|
|
@ -491,3 +502,21 @@ class ExtractOTIOReview(publish.Extractor):
|
|||
out_frame_start = self.used_frames[-1]
|
||||
|
||||
return output_path, out_frame_start
|
||||
|
||||
def _get_folder_name_based_prefix(self, instance):
|
||||
"""Creates 'unique' human readable file prefix to differentiate.
|
||||
|
||||
Multiple instances might share same temp folder, but each instance
|
||||
would be differentiated by asset, eg. folder name.
|
||||
|
||||
It ix expected that there won't be multiple instances for same asset.
|
||||
"""
|
||||
folder_path = instance.data["folderPath"]
|
||||
folder_name = folder_path.split("/")[-1]
|
||||
folder_path = folder_path.replace("/", "_").lstrip("_")
|
||||
|
||||
file_prefix = f"{folder_path}_{folder_name}."
|
||||
self.log.debug(f"file_prefix::{file_prefix}")
|
||||
|
||||
return file_prefix
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -1900,7 +1900,7 @@ class OverscanCrop:
|
|||
string_value = re.sub(r"([ ]+)?px", " ", string_value)
|
||||
string_value = re.sub(r"([ ]+)%", "%", string_value)
|
||||
# Make sure +/- sign at the beginning of string is next to number
|
||||
string_value = re.sub(r"^([\+\-])[ ]+", "\g<1>", string_value)
|
||||
string_value = re.sub(r"^([\+\-])[ ]+", r"\g<1>", string_value)
|
||||
# Make sure +/- sign in the middle has zero spaces before number under
|
||||
# which belongs
|
||||
string_value = re.sub(
|
||||
|
|
|
|||
|
|
@ -455,6 +455,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin):
|
|||
# output file
|
||||
jpeg_items.append(path_to_subprocess_arg(dst_path))
|
||||
subprocess_command = " ".join(jpeg_items)
|
||||
|
||||
try:
|
||||
run_subprocess(
|
||||
subprocess_command, shell=True, logger=self.log
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import os
|
|||
from typing import Dict
|
||||
|
||||
import pyblish.api
|
||||
from pxr import Sdf
|
||||
try:
|
||||
from pxr import Sdf
|
||||
except ImportError:
|
||||
Sdf = None
|
||||
|
||||
from ayon_core.lib import (
|
||||
TextDef,
|
||||
|
|
@ -13,21 +16,24 @@ from ayon_core.lib import (
|
|||
UILabelDef,
|
||||
EnumDef
|
||||
)
|
||||
from ayon_core.pipeline.usdlib import (
|
||||
get_or_define_prim_spec,
|
||||
add_ordered_reference,
|
||||
variant_nested_prim_path,
|
||||
setup_asset_layer,
|
||||
add_ordered_sublayer,
|
||||
set_layer_defaults
|
||||
)
|
||||
try:
|
||||
from ayon_core.pipeline.usdlib import (
|
||||
get_or_define_prim_spec,
|
||||
add_ordered_reference,
|
||||
variant_nested_prim_path,
|
||||
setup_asset_layer,
|
||||
add_ordered_sublayer,
|
||||
set_layer_defaults
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
from ayon_core.pipeline.entity_uri import (
|
||||
construct_ayon_entity_uri,
|
||||
parse_ayon_entity_uri
|
||||
)
|
||||
from ayon_core.pipeline.load.utils import get_representation_path_by_names
|
||||
from ayon_core.pipeline.publish.lib import get_instance_expected_output_path
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_core.pipeline import publish, KnownPublishError
|
||||
|
||||
|
||||
# This global toggle is here mostly for debugging purposes and should usually
|
||||
|
|
@ -77,7 +83,7 @@ def get_representation_path_in_publish_context(
|
|||
|
||||
Allow resolving 'latest' paths from a publishing context's instances
|
||||
as if they will exist after publishing without them being integrated yet.
|
||||
|
||||
|
||||
Use first instance that has same folder path and product name,
|
||||
and contains representation with passed name.
|
||||
|
||||
|
|
@ -138,13 +144,14 @@ def get_instance_uri_path(
|
|||
folder_path = instance.data["folderPath"]
|
||||
product_name = instance.data["productName"]
|
||||
project_name = context.data["projectName"]
|
||||
version_name = instance.data["version"]
|
||||
|
||||
# Get the layer's published path
|
||||
path = construct_ayon_entity_uri(
|
||||
project_name=project_name,
|
||||
folder_path=folder_path,
|
||||
product=product_name,
|
||||
version="latest",
|
||||
version=version_name,
|
||||
representation_name="usd"
|
||||
)
|
||||
|
||||
|
|
@ -231,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
|
||||
|
|
@ -555,12 +562,24 @@ class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions):
|
|||
return defs
|
||||
|
||||
|
||||
class ValidateUSDDependencies(pyblish.api.InstancePlugin):
|
||||
families = ["usdLayer"]
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
|
||||
def process(self, instance):
|
||||
if Sdf is None:
|
||||
raise KnownPublishError("USD library 'Sdf' is not available.")
|
||||
|
||||
|
||||
class ExtractUSDLayerContribution(publish.Extractor):
|
||||
|
||||
families = ["usdLayer"]
|
||||
label = "Extract USD Layer Contributions (Asset/Shot)"
|
||||
order = pyblish.api.ExtractorOrder + 0.45
|
||||
|
||||
use_ayon_entity_uri = False
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
folder_path = instance.data["folderPath"]
|
||||
|
|
@ -578,7 +597,8 @@ class ExtractUSDLayerContribution(publish.Extractor):
|
|||
|
||||
contributions = instance.data.get("usd_contributions", [])
|
||||
for contribution in sorted(contributions, key=attrgetter("order")):
|
||||
path = get_instance_uri_path(contribution.instance)
|
||||
path = get_instance_uri_path(contribution.instance,
|
||||
resolve=not self.use_ayon_entity_uri)
|
||||
if isinstance(contribution, VariantContribution):
|
||||
# Add contribution as a reference inside a variant
|
||||
self.log.debug(f"Adding variant: {contribution}")
|
||||
|
|
@ -652,14 +672,14 @@ class ExtractUSDLayerContribution(publish.Extractor):
|
|||
)
|
||||
|
||||
def remove_previous_reference_contribution(self,
|
||||
prim_spec: Sdf.PrimSpec,
|
||||
prim_spec: "Sdf.PrimSpec",
|
||||
instance: pyblish.api.Instance):
|
||||
# Remove existing contributions of the same product - ignoring
|
||||
# the picked version and representation. We assume there's only ever
|
||||
# one version of a product you want to have referenced into a Prim.
|
||||
remove_indices = set()
|
||||
for index, ref in enumerate(prim_spec.referenceList.prependedItems):
|
||||
ref: Sdf.Reference # type hint
|
||||
ref: "Sdf.Reference"
|
||||
|
||||
uri = ref.customData.get("ayon_uri")
|
||||
if uri and self.instance_match_ayon_uri(instance, uri):
|
||||
|
|
@ -674,8 +694,8 @@ class ExtractUSDLayerContribution(publish.Extractor):
|
|||
]
|
||||
|
||||
def add_reference_contribution(self,
|
||||
layer: Sdf.Layer,
|
||||
prim_path: Sdf.Path,
|
||||
layer: "Sdf.Layer",
|
||||
prim_path: "Sdf.Path",
|
||||
filepath: str,
|
||||
contribution: VariantContribution):
|
||||
instance = contribution.instance
|
||||
|
|
@ -720,6 +740,8 @@ class ExtractUSDAssetContribution(publish.Extractor):
|
|||
label = "Extract USD Asset/Shot Contributions"
|
||||
order = ExtractUSDLayerContribution.order + 0.01
|
||||
|
||||
use_ayon_entity_uri = False
|
||||
|
||||
def process(self, instance):
|
||||
|
||||
folder_path = instance.data["folderPath"]
|
||||
|
|
@ -795,15 +817,15 @@ class ExtractUSDAssetContribution(publish.Extractor):
|
|||
layer_id = layer_instance.data["usd_layer_id"]
|
||||
order = layer_instance.data["usd_layer_order"]
|
||||
|
||||
path = get_instance_uri_path(instance=layer_instance)
|
||||
path = get_instance_uri_path(instance=layer_instance,
|
||||
resolve=not self.use_ayon_entity_uri)
|
||||
add_ordered_sublayer(target_layer,
|
||||
contribution_path=path,
|
||||
layer_id=layer_id,
|
||||
order=order,
|
||||
# Add the sdf argument metadata which allows
|
||||
# us to later detect whether another path
|
||||
# has the same layer id, so we can replace it
|
||||
# it.
|
||||
# has the same layer id, so we can replace it.
|
||||
add_sdf_arguments_metadata=True)
|
||||
|
||||
# Save the file
|
||||
|
|
|
|||
|
|
@ -1,17 +1,59 @@
|
|||
import inspect
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline.publish import PublishValidationError
|
||||
from ayon_core.tools.utils.host_tools import show_workfiles
|
||||
from ayon_core.pipeline.context_tools import version_up_current_workfile
|
||||
|
||||
|
||||
class SaveByVersionUpAction(pyblish.api.Action):
|
||||
"""Save Workfile."""
|
||||
label = "Save Workfile"
|
||||
on = "failed"
|
||||
icon = "save"
|
||||
|
||||
def process(self, context, plugin):
|
||||
version_up_current_workfile()
|
||||
|
||||
|
||||
class ShowWorkfilesAction(pyblish.api.Action):
|
||||
"""Save Workfile."""
|
||||
label = "Show Workfiles Tool..."
|
||||
on = "failed"
|
||||
icon = "files-o"
|
||||
|
||||
def process(self, context, plugin):
|
||||
show_workfiles()
|
||||
|
||||
|
||||
class ValidateCurrentSaveFile(pyblish.api.ContextPlugin):
|
||||
"""File must be saved before publishing"""
|
||||
"""File must be saved before publishing
|
||||
|
||||
This does not validate for unsaved changes. It only validates whether
|
||||
the current context was able to identify any 'currentFile'.
|
||||
"""
|
||||
|
||||
label = "Validate File Saved"
|
||||
order = pyblish.api.ValidatorOrder - 0.1
|
||||
hosts = ["maya", "houdini", "nuke"]
|
||||
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter"]
|
||||
actions = [SaveByVersionUpAction, ShowWorkfilesAction]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
current_file = context.data["currentFile"]
|
||||
if not current_file:
|
||||
raise PublishValidationError("File not saved")
|
||||
raise PublishValidationError(
|
||||
"Workfile is not saved. Please save your scene to continue.",
|
||||
title="File not saved",
|
||||
description=self.get_description())
|
||||
|
||||
def get_description(self):
|
||||
return inspect.cleandoc("""
|
||||
### File not saved
|
||||
|
||||
Your workfile must be saved to continue publishing.
|
||||
|
||||
The **Save Workfile** action will save it for you with the first
|
||||
available workfile version number in your current context.
|
||||
""")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ from typing import (
|
|||
|
||||
from ayon_core.lib import AbstractAttrDef
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.pipeline.create import CreateContext, CreatedInstance
|
||||
from ayon_core.pipeline.create.context import ConvertorItem
|
||||
from ayon_core.pipeline.create import (
|
||||
CreateContext,
|
||||
CreatedInstance,
|
||||
ConvertorItem,
|
||||
)
|
||||
from ayon_core.tools.common_models import (
|
||||
FolderItem,
|
||||
TaskItem,
|
||||
|
|
@ -319,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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from ayon_core.pipeline.create import (
|
|||
CreateContext,
|
||||
CreatedInstance,
|
||||
)
|
||||
from ayon_core.pipeline.create.context import (
|
||||
from ayon_core.pipeline.create import (
|
||||
CreatorsOperationFailed,
|
||||
ConvertorsOperationFailed,
|
||||
ConvertorItem,
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,171 @@
|
|||
import os
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
from qtpy import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class ScreenMarquee(QtWidgets.QDialog):
|
||||
class ScreenMarqueeDialog(QtWidgets.QDialog):
|
||||
mouse_moved = QtCore.Signal()
|
||||
mouse_pressed = QtCore.Signal(QtCore.QPoint, str)
|
||||
mouse_released = QtCore.Signal(QtCore.QPoint)
|
||||
close_requested = QtCore.Signal()
|
||||
|
||||
def __init__(self, screen: QtCore.QObject, screen_id: str):
|
||||
super().__init__()
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.Window
|
||||
| QtCore.Qt.FramelessWindowHint
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
| QtCore.Qt.CustomizeWindowHint
|
||||
)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setCursor(QtCore.Qt.CrossCursor)
|
||||
self.setMouseTracking(True)
|
||||
|
||||
screen.geometryChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
self._screen = screen
|
||||
self._opacity = 100
|
||||
self._click_pos = None
|
||||
self._screen_id = screen_id
|
||||
|
||||
def set_click_pos(self, pos):
|
||||
self._click_pos = pos
|
||||
self.repaint()
|
||||
|
||||
def convert_end_pos(self, pos):
|
||||
glob_pos = self.mapFromGlobal(pos)
|
||||
new_pos = self._convert_pos(glob_pos)
|
||||
return self.mapToGlobal(new_pos)
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Paint event"""
|
||||
# Convert click and current mouse positions to local space.
|
||||
mouse_pos = self._convert_pos(self.mapFromGlobal(QtGui.QCursor.pos()))
|
||||
|
||||
rect = event.rect()
|
||||
fill_path = QtGui.QPainterPath()
|
||||
fill_path.addRect(rect)
|
||||
|
||||
capture_rect = None
|
||||
if self._click_pos is not None:
|
||||
click_pos = self.mapFromGlobal(self._click_pos)
|
||||
capture_rect = QtCore.QRect(click_pos, mouse_pos)
|
||||
|
||||
# Clear the capture area
|
||||
sub_path = QtGui.QPainterPath()
|
||||
sub_path.addRect(capture_rect)
|
||||
fill_path = fill_path.subtracted(sub_path)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHints(
|
||||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
|
||||
# Draw background. Aside from aesthetics, this makes the full
|
||||
# tool region accept mouse events.
|
||||
painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity))
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
painter.drawPath(fill_path)
|
||||
|
||||
# Draw cropping markers at current mouse position
|
||||
pen_color = QtGui.QColor(255, 255, 255, self._opacity)
|
||||
pen = QtGui.QPen(pen_color, 1, QtCore.Qt.DotLine)
|
||||
painter.setPen(pen)
|
||||
painter.drawLine(
|
||||
rect.left(), mouse_pos.y(),
|
||||
rect.right(), mouse_pos.y()
|
||||
)
|
||||
painter.drawLine(
|
||||
mouse_pos.x(), rect.top(),
|
||||
mouse_pos.x(), rect.bottom()
|
||||
)
|
||||
|
||||
# Draw rectangle around selection area
|
||||
if capture_rect is not None:
|
||||
pen_color = QtGui.QColor(92, 173, 214)
|
||||
pen = QtGui.QPen(pen_color, 2)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(QtCore.Qt.NoBrush)
|
||||
l_x = capture_rect.left()
|
||||
r_x = capture_rect.right()
|
||||
if l_x > r_x:
|
||||
l_x, r_x = r_x, l_x
|
||||
t_y = capture_rect.top()
|
||||
b_y = capture_rect.bottom()
|
||||
if t_y > b_y:
|
||||
t_y, b_y = b_y, t_y
|
||||
|
||||
# -1 to draw 1px over the border
|
||||
r_x -= 1
|
||||
b_y -= 1
|
||||
sel_rect = QtCore.QRect(
|
||||
QtCore.QPoint(l_x, t_y),
|
||||
QtCore.QPoint(r_x, b_y)
|
||||
)
|
||||
painter.drawRect(sel_rect)
|
||||
|
||||
painter.end()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Mouse click event"""
|
||||
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
# Begin click drag operation
|
||||
self._click_pos = event.globalPos()
|
||||
self.mouse_pressed.emit(self._click_pos, self._screen_id)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""Mouse release event"""
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
# End click drag operation and commit the current capture rect
|
||||
self._click_pos = None
|
||||
self.mouse_released.emit(event.globalPos())
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Mouse move event"""
|
||||
self.mouse_moved.emit()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Mouse press event"""
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self._click_pos = None
|
||||
event.accept()
|
||||
self.close_requested.emit()
|
||||
return
|
||||
return super().keyPressEvent(event)
|
||||
|
||||
def showEvent(self, event):
|
||||
super().showEvent(event)
|
||||
self._fit_screen_geometry()
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._click_pos = None
|
||||
super().closeEvent(event)
|
||||
|
||||
def _convert_pos(self, pos):
|
||||
geo = self.geometry()
|
||||
if pos.x() > geo.width():
|
||||
pos.setX(geo.width() - 1)
|
||||
elif pos.x() < 0:
|
||||
pos.setX(0)
|
||||
|
||||
if pos.y() > geo.height():
|
||||
pos.setY(geo.height() - 1)
|
||||
elif pos.y() < 0:
|
||||
pos.setY(0)
|
||||
return pos
|
||||
|
||||
def _fit_screen_geometry(self):
|
||||
# On macOs it is required to set screen explicitly
|
||||
if hasattr(self, "setScreen"):
|
||||
self.setScreen(self._screen)
|
||||
self.setGeometry(self._screen.geometry())
|
||||
|
||||
|
||||
class ScreenMarquee(QtCore.QObject):
|
||||
"""Dialog to interactively define screen area.
|
||||
|
||||
This allows to select a screen area through a marquee selection.
|
||||
|
|
@ -17,187 +178,186 @@ class ScreenMarquee(QtWidgets.QDialog):
|
|||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.Window
|
||||
| QtCore.Qt.FramelessWindowHint
|
||||
| QtCore.Qt.WindowStaysOnTopHint
|
||||
| QtCore.Qt.CustomizeWindowHint
|
||||
)
|
||||
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||
self.setCursor(QtCore.Qt.CrossCursor)
|
||||
self.setMouseTracking(True)
|
||||
|
||||
app = QtWidgets.QApplication.instance()
|
||||
if hasattr(app, "screenAdded"):
|
||||
app.screenAdded.connect(self._on_screen_added)
|
||||
app.screenRemoved.connect(self._fit_screen_geometry)
|
||||
elif hasattr(app, "desktop"):
|
||||
desktop = app.desktop()
|
||||
desktop.screenCountChanged.connect(self._fit_screen_geometry)
|
||||
|
||||
screens_by_id = {}
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
screen.geometryChanged.connect(self._fit_screen_geometry)
|
||||
screen_id = uuid.uuid4().hex
|
||||
screen_dialog = ScreenMarqueeDialog(screen, screen_id)
|
||||
screens_by_id[screen_id] = screen_dialog
|
||||
screen_dialog.mouse_moved.connect(self._on_mouse_move)
|
||||
screen_dialog.mouse_pressed.connect(self._on_mouse_press)
|
||||
screen_dialog.mouse_released.connect(self._on_mouse_release)
|
||||
screen_dialog.close_requested.connect(self._on_close_request)
|
||||
|
||||
self._opacity = 50
|
||||
self._click_pos = None
|
||||
self._capture_rect = None
|
||||
self._screens_by_id = screens_by_id
|
||||
self._finished = False
|
||||
self._captured = False
|
||||
self._start_pos = None
|
||||
self._end_pos = None
|
||||
self._start_screen_id = None
|
||||
self._pix = None
|
||||
|
||||
def get_captured_pixmap(self):
|
||||
if self._capture_rect is None:
|
||||
if self._pix is None:
|
||||
return QtGui.QPixmap()
|
||||
return self._pix
|
||||
|
||||
return self.get_desktop_pixmap(self._capture_rect)
|
||||
def _close_dialogs(self):
|
||||
for dialog in self._screens_by_id.values():
|
||||
dialog.close()
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Paint event"""
|
||||
def _on_close_request(self):
|
||||
self._close_dialogs()
|
||||
self._finished = True
|
||||
|
||||
# Convert click and current mouse positions to local space.
|
||||
mouse_pos = self.mapFromGlobal(QtGui.QCursor.pos())
|
||||
click_pos = None
|
||||
if self._click_pos is not None:
|
||||
click_pos = self.mapFromGlobal(self._click_pos)
|
||||
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHints(
|
||||
QtGui.QPainter.Antialiasing
|
||||
| QtGui.QPainter.SmoothPixmapTransform
|
||||
)
|
||||
|
||||
# Draw background. Aside from aesthetics, this makes the full
|
||||
# tool region accept mouse events.
|
||||
painter.setBrush(QtGui.QColor(0, 0, 0, self._opacity))
|
||||
painter.setPen(QtCore.Qt.NoPen)
|
||||
rect = event.rect()
|
||||
fill_path = QtGui.QPainterPath()
|
||||
fill_path.addRect(rect)
|
||||
|
||||
# Clear the capture area
|
||||
if click_pos is not None:
|
||||
sub_path = QtGui.QPainterPath()
|
||||
capture_rect = QtCore.QRect(click_pos, mouse_pos)
|
||||
sub_path.addRect(capture_rect)
|
||||
fill_path = fill_path.subtracted(sub_path)
|
||||
|
||||
painter.drawPath(fill_path)
|
||||
|
||||
pen_color = QtGui.QColor(255, 255, 255, self._opacity)
|
||||
pen = QtGui.QPen(pen_color, 1, QtCore.Qt.DotLine)
|
||||
painter.setPen(pen)
|
||||
|
||||
# Draw cropping markers at click position
|
||||
if click_pos is not None:
|
||||
painter.drawLine(
|
||||
rect.left(), click_pos.y(),
|
||||
rect.right(), click_pos.y()
|
||||
)
|
||||
painter.drawLine(
|
||||
click_pos.x(), rect.top(),
|
||||
click_pos.x(), rect.bottom()
|
||||
)
|
||||
|
||||
# Draw cropping markers at current mouse position
|
||||
painter.drawLine(
|
||||
rect.left(), mouse_pos.y(),
|
||||
rect.right(), mouse_pos.y()
|
||||
)
|
||||
painter.drawLine(
|
||||
mouse_pos.x(), rect.top(),
|
||||
mouse_pos.x(), rect.bottom()
|
||||
)
|
||||
painter.end()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
"""Mouse click event"""
|
||||
|
||||
if event.button() == QtCore.Qt.LeftButton:
|
||||
# Begin click drag operation
|
||||
self._click_pos = event.globalPos()
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
"""Mouse release event"""
|
||||
if (
|
||||
self._click_pos is not None
|
||||
and event.button() == QtCore.Qt.LeftButton
|
||||
):
|
||||
# End click drag operation and commit the current capture rect
|
||||
self._capture_rect = QtCore.QRect(
|
||||
self._click_pos, event.globalPos()
|
||||
).normalized()
|
||||
self._click_pos = None
|
||||
self.close()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
"""Mouse move event"""
|
||||
self.repaint()
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
"""Mouse press event"""
|
||||
if event.key() == QtCore.Qt.Key_Escape:
|
||||
self._click_pos = None
|
||||
self._capture_rect = None
|
||||
event.accept()
|
||||
self.close()
|
||||
def _on_mouse_release(self, pos):
|
||||
start_screen_dialog = self._screens_by_id.get(self._start_screen_id)
|
||||
if start_screen_dialog is None:
|
||||
self._finished = True
|
||||
self._captured = False
|
||||
return
|
||||
return super().keyPressEvent(event)
|
||||
|
||||
def showEvent(self, event):
|
||||
self._fit_screen_geometry()
|
||||
end_pos = start_screen_dialog.convert_end_pos(pos)
|
||||
|
||||
def _fit_screen_geometry(self):
|
||||
# Compute the union of all screen geometries, and resize to fit.
|
||||
workspace_rect = QtCore.QRect()
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
workspace_rect = workspace_rect.united(screen.geometry())
|
||||
self.setGeometry(workspace_rect)
|
||||
self._close_dialogs()
|
||||
self._end_pos = end_pos
|
||||
self._finished = True
|
||||
self._captured = True
|
||||
|
||||
def _on_screen_added(self):
|
||||
for screen in QtGui.QGuiApplication.screens():
|
||||
screen.geometryChanged.connect(self._fit_screen_geometry)
|
||||
def _on_mouse_press(self, pos, screen_id):
|
||||
self._start_pos = pos
|
||||
self._start_screen_id = screen_id
|
||||
|
||||
def _on_mouse_move(self):
|
||||
for dialog in self._screens_by_id.values():
|
||||
dialog.repaint()
|
||||
|
||||
def start_capture(self):
|
||||
for dialog in self._screens_by_id.values():
|
||||
dialog.show()
|
||||
# Activate so Escape event is not ignored.
|
||||
dialog.setWindowState(QtCore.Qt.WindowActive)
|
||||
|
||||
app = QtWidgets.QApplication.instance()
|
||||
while not self._finished:
|
||||
app.processEvents()
|
||||
|
||||
# Give time to cloe dialogs
|
||||
for _ in range(2):
|
||||
app.processEvents()
|
||||
|
||||
if self._captured:
|
||||
self._pix = self.get_desktop_pixmap(
|
||||
self._start_pos, self._end_pos
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_desktop_pixmap(cls, rect):
|
||||
def get_desktop_pixmap(cls, pos_start, pos_end):
|
||||
"""Performs a screen capture on the specified rectangle.
|
||||
|
||||
Args:
|
||||
rect (QtCore.QRect): The rectangle to capture.
|
||||
pos_start (QtCore.QPoint): Start of screen capture.
|
||||
pos_end (QtCore.QPoint): End of screen capture.
|
||||
|
||||
Returns:
|
||||
QtGui.QPixmap: Captured pixmap image
|
||||
"""
|
||||
|
||||
"""
|
||||
# Unify start and end points
|
||||
# - start is top left
|
||||
# - end is bottom right
|
||||
if pos_start.y() > pos_end.y():
|
||||
pos_start, pos_end = pos_end, pos_start
|
||||
|
||||
if pos_start.x() > pos_end.x():
|
||||
new_start = QtCore.QPoint(pos_end.x(), pos_start.y())
|
||||
new_end = QtCore.QPoint(pos_start.x(), pos_end.y())
|
||||
pos_start = new_start
|
||||
pos_end = new_end
|
||||
|
||||
# Validate if the rectangle is valid
|
||||
rect = QtCore.QRect(pos_start, pos_end)
|
||||
if rect.width() < 1 or rect.height() < 1:
|
||||
return QtGui.QPixmap()
|
||||
|
||||
screen_pixes = []
|
||||
for screen in QtWidgets.QApplication.screens():
|
||||
screen_geo = screen.geometry()
|
||||
if not screen_geo.intersects(rect):
|
||||
continue
|
||||
screen = QtWidgets.QApplication.screenAt(pos_start)
|
||||
return screen.grabWindow(
|
||||
0,
|
||||
pos_start.x() - screen.geometry().x(),
|
||||
pos_start.y() - screen.geometry().y(),
|
||||
pos_end.x() - pos_start.x(),
|
||||
pos_end.y() - pos_start.y()
|
||||
)
|
||||
# Multiscreen capture that does not work
|
||||
# - does not handle pixel aspect ratio and positioning of screens
|
||||
|
||||
screen_pix_rect = screen_geo.intersected(rect)
|
||||
screen_pix = screen.grabWindow(
|
||||
0,
|
||||
screen_pix_rect.x() - screen_geo.x(),
|
||||
screen_pix_rect.y() - screen_geo.y(),
|
||||
screen_pix_rect.width(), screen_pix_rect.height()
|
||||
)
|
||||
paste_point = QtCore.QPoint(
|
||||
screen_pix_rect.x() - rect.x(),
|
||||
screen_pix_rect.y() - rect.y()
|
||||
)
|
||||
screen_pixes.append((screen_pix, paste_point))
|
||||
|
||||
output_pix = QtGui.QPixmap(rect.width(), rect.height())
|
||||
output_pix.fill(QtCore.Qt.transparent)
|
||||
pix_painter = QtGui.QPainter()
|
||||
pix_painter.begin(output_pix)
|
||||
for item in screen_pixes:
|
||||
(screen_pix, offset) = item
|
||||
pix_painter.drawPixmap(offset, screen_pix)
|
||||
|
||||
pix_painter.end()
|
||||
|
||||
return output_pix
|
||||
# most_left = None
|
||||
# most_top = None
|
||||
# for screen in QtWidgets.QApplication.screens():
|
||||
# screen_geo = screen.geometry()
|
||||
# if most_left is None or most_left > screen_geo.x():
|
||||
# most_left = screen_geo.x()
|
||||
#
|
||||
# if most_top is None or most_top > screen_geo.y():
|
||||
# most_top = screen_geo.y()
|
||||
#
|
||||
# most_left = most_left or 0
|
||||
# most_top = most_top or 0
|
||||
#
|
||||
# screen_pixes = []
|
||||
# for screen in QtWidgets.QApplication.screens():
|
||||
# screen_geo = screen.geometry()
|
||||
# if not screen_geo.intersects(rect):
|
||||
# continue
|
||||
#
|
||||
# pos_l_x = screen_geo.x()
|
||||
# pos_l_y = screen_geo.y()
|
||||
# pos_r_x = screen_geo.x() + screen_geo.width()
|
||||
# pos_r_y = screen_geo.y() + screen_geo.height()
|
||||
# if pos_start.x() > pos_l_x:
|
||||
# pos_l_x = pos_start.x()
|
||||
#
|
||||
# if pos_start.y() > pos_l_y:
|
||||
# pos_l_y = pos_start.y()
|
||||
#
|
||||
# if pos_end.x() < pos_r_x:
|
||||
# pos_r_x = pos_end.x()
|
||||
#
|
||||
# if pos_end.y() < pos_r_y:
|
||||
# pos_r_y = pos_end.y()
|
||||
#
|
||||
# capture_pos_x = pos_l_x - screen_geo.x()
|
||||
# capture_pos_y = pos_l_y - screen_geo.y()
|
||||
# capture_screen_width = pos_r_x - pos_l_x
|
||||
# capture_screen_height = pos_r_y - pos_l_y
|
||||
# screen_pix = screen.grabWindow(
|
||||
# 0,
|
||||
# capture_pos_x, capture_pos_y,
|
||||
# capture_screen_width, capture_screen_height
|
||||
# )
|
||||
# paste_point = QtCore.QPoint(
|
||||
# (pos_l_x - screen_geo.x()) - rect.x(),
|
||||
# (pos_l_y - screen_geo.y()) - rect.y()
|
||||
# )
|
||||
# screen_pixes.append((screen_pix, paste_point))
|
||||
#
|
||||
# output_pix = QtGui.QPixmap(rect.width(), rect.height())
|
||||
# output_pix.fill(QtCore.Qt.transparent)
|
||||
# pix_painter = QtGui.QPainter()
|
||||
# pix_painter.begin(output_pix)
|
||||
# render_hints = (
|
||||
# QtGui.QPainter.Antialiasing
|
||||
# | QtGui.QPainter.SmoothPixmapTransform
|
||||
# )
|
||||
# if hasattr(QtGui.QPainter, "HighQualityAntialiasing"):
|
||||
# render_hints |= QtGui.QPainter.HighQualityAntialiasing
|
||||
# pix_painter.setRenderHints(render_hints)
|
||||
# for item in screen_pixes:
|
||||
# (screen_pix, offset) = item
|
||||
# pix_painter.drawPixmap(offset, screen_pix)
|
||||
#
|
||||
# pix_painter.end()
|
||||
#
|
||||
# return output_pix
|
||||
|
||||
@classmethod
|
||||
def capture_to_pixmap(cls):
|
||||
|
|
@ -209,12 +369,8 @@ class ScreenMarquee(QtWidgets.QDialog):
|
|||
Returns:
|
||||
QtGui.QPixmap: Captured pixmap image.
|
||||
"""
|
||||
|
||||
tool = cls()
|
||||
# Activate so Escape event is not ignored.
|
||||
tool.setWindowState(QtCore.Qt.WindowActive)
|
||||
# Exec dialog and return captured pixmap.
|
||||
tool.exec_()
|
||||
tool.start_capture()
|
||||
return tool.get_captured_pixmap()
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -578,7 +578,7 @@ def make_sure_tray_is_running(
|
|||
args = get_ayon_launcher_args("tray", "--force")
|
||||
if env is None:
|
||||
env = os.environ.copy()
|
||||
|
||||
|
||||
# Make sure 'QT_API' is not set
|
||||
env.pop("QT_API", None)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ def main(title="Scripts", parent=None, objectName=None):
|
|||
|
||||
# Register control + shift callback to add to shelf (maya behavior)
|
||||
modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier
|
||||
if int(cmds.about(version=True)) <= 2025:
|
||||
if int(cmds.about(version=True)) < 2025:
|
||||
modifiers = int(modifiers)
|
||||
|
||||
menu.register_callback(modifiers, to_shelf)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON core addon version."""
|
||||
__version__ = "0.4.4-dev.1"
|
||||
__version__ = "0.4.5-dev.1"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
name = "core"
|
||||
title = "Core"
|
||||
version = "0.4.4-dev.1"
|
||||
version = "0.4.5-dev.1"
|
||||
|
||||
client_dir = "ayon_core"
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ target-version = "py39"
|
|||
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
select = ["E4", "E7", "E9", "F", "W"]
|
||||
ignore = []
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
|
|
@ -89,7 +89,6 @@ exclude = [
|
|||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"client/ayon_core/lib/__init__.py" = ["E402"]
|
||||
"client/ayon_core/hosts/max/startup/startup.py" = ["E402"]
|
||||
|
||||
[tool.ruff.format]
|
||||
# Like Black, use double quotes for strings.
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class CollectFramesFixDefModel(BaseSettingsModel):
|
|||
True,
|
||||
title="Show 'Rewrite latest version' toggle"
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ContributionLayersModel(BaseSettingsModel):
|
||||
_layout = "compact"
|
||||
|
|
@ -84,6 +84,17 @@ class CollectUSDLayerContributionsModel(BaseSettingsModel):
|
|||
return value
|
||||
|
||||
|
||||
class AyonEntityURIModel(BaseSettingsModel):
|
||||
use_ayon_entity_uri: bool = SettingsField(
|
||||
title="Use AYON Entity URI",
|
||||
description=(
|
||||
"When enabled the USD paths written using the contribution "
|
||||
"workflow will use ayon entity URIs instead of resolved published "
|
||||
"paths. You can only load these if you use the AYON USD Resolver."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PluginStateByHostModelProfile(BaseSettingsModel):
|
||||
_layout = "expanded"
|
||||
# Filtering
|
||||
|
|
@ -857,6 +868,14 @@ class PublishPuginsModel(BaseSettingsModel):
|
|||
default_factory=ExtractBurninModel,
|
||||
title="Extract Burnin"
|
||||
)
|
||||
ExtractUSDAssetContribution: AyonEntityURIModel = SettingsField(
|
||||
default_factory=AyonEntityURIModel,
|
||||
title="Extract USD Asset Contribution",
|
||||
)
|
||||
ExtractUSDLayerContribution: AyonEntityURIModel = SettingsField(
|
||||
default_factory=AyonEntityURIModel,
|
||||
title="Extract USD Layer Contribution",
|
||||
)
|
||||
PreIntegrateThumbnails: PreIntegrateThumbnailsModel = SettingsField(
|
||||
default_factory=PreIntegrateThumbnailsModel,
|
||||
title="Override Integrate Thumbnail Representations"
|
||||
|
|
@ -1167,6 +1186,12 @@ DEFAULT_PUBLISH_VALUES = {
|
|||
}
|
||||
]
|
||||
},
|
||||
"ExtractUSDAssetContribution": {
|
||||
"use_ayon_entity_uri": False,
|
||||
},
|
||||
"ExtractUSDLayerContribution": {
|
||||
"use_ayon_entity_uri": False,
|
||||
},
|
||||
"PreIntegrateThumbnails": {
|
||||
"enabled": True,
|
||||
"integrate_profiles": []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue